├── .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 │ ├── release.yml │ ├── reproduire-close.yml │ └── reproduire.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── eslint.config.js ├── logo.svg ├── maintainers.md ├── package.json ├── packages └── runed │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── setupTest.ts │ ├── src │ ├── app.d.ts │ ├── app.html │ ├── index.test.ts │ └── lib │ │ ├── index.ts │ │ ├── internal │ │ ├── configurable-globals.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── array.ts │ │ │ ├── browser.ts │ │ │ ├── dom.test.ts │ │ │ ├── dom.ts │ │ │ ├── function.ts │ │ │ ├── get.ts │ │ │ ├── is.ts │ │ │ └── sleep.ts │ │ ├── test │ │ └── util.svelte.ts │ │ └── utilities │ │ ├── active-element │ │ ├── active-element.svelte.ts │ │ ├── active-element.test.svelte.ts │ │ └── index.ts │ │ ├── animation-frames │ │ ├── animation-frames.svelte.ts │ │ └── index.ts │ │ ├── context │ │ ├── context.ts │ │ └── index.ts │ │ ├── debounced │ │ ├── debounced.svelte.ts │ │ ├── debounced.test.svelte.ts │ │ └── index.ts │ │ ├── element-rect │ │ ├── element-rect.svelte.ts │ │ └── index.ts │ │ ├── element-size │ │ ├── element-size.svelte.ts │ │ └── index.ts │ │ ├── extract │ │ ├── extract.svelte.ts │ │ └── index.ts │ │ ├── finite-state-machine │ │ ├── finite-state-machine.svelte.ts │ │ ├── finite-state-machine.test.svelte.ts │ │ └── index.ts │ │ ├── index.ts │ │ ├── is-focus-within │ │ ├── index.ts │ │ ├── is-focus-within.svelte.ts │ │ └── is-focus-within.test.svelte.ts │ │ ├── is-idle │ │ ├── index.ts │ │ ├── is-idle.svelte.ts │ │ └── is-idle.test.svelte.ts │ │ ├── is-in-viewport │ │ ├── index.ts │ │ └── is-in-viewport.svelte.ts │ │ ├── is-mounted │ │ ├── index.ts │ │ └── is-mounted.svelte.ts │ │ ├── on-click-outside │ │ ├── index.ts │ │ ├── on-click-outside.svelte.ts │ │ └── on-click-outside.test.svelte.ts │ │ ├── persisted-state │ │ ├── index.ts │ │ ├── persisted-state.svelte.ts │ │ └── persisted-state.test.svelte.ts │ │ ├── pressed-keys │ │ ├── index.ts │ │ └── pressed-keys.svelte.ts │ │ ├── previous │ │ ├── index.ts │ │ ├── previous.svelte.ts │ │ └── previous.test.svelte.ts │ │ ├── resource │ │ ├── index.ts │ │ ├── msw-handlers.ts │ │ ├── resource.svelte.ts │ │ └── resource.test.svelte.ts │ │ ├── scroll-state │ │ ├── index.ts │ │ └── scroll-state.svelte.ts │ │ ├── state-history │ │ ├── index.ts │ │ ├── state-history.svelte.ts │ │ └── state-history.test.svelte.ts │ │ ├── textarea-autosize │ │ ├── index.ts │ │ └── textarea-autosize.svelte.ts │ │ ├── use-debounce │ │ ├── index.ts │ │ ├── use-debounce.svelte.ts │ │ └── use-debounce.test.svelte.ts │ │ ├── use-event-listener │ │ ├── index.ts │ │ └── use-event-listener.svelte.ts │ │ ├── use-geolocation │ │ ├── index.ts │ │ └── use-geolocation.svelte.ts │ │ ├── use-intersection-observer │ │ ├── index.ts │ │ └── use-intersection-observer.svelte.ts │ │ ├── use-mutation-observer │ │ ├── index.ts │ │ └── use-mutation-observer.svelte.ts │ │ ├── use-resize-observer │ │ ├── index.ts │ │ └── use-resize-observer.svelte.ts │ │ └── watch │ │ ├── index.ts │ │ ├── watch.svelte.ts │ │ └── watch.test.svelte.ts │ ├── svelte.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts └── add-utility.mjs └── sites └── docs ├── .gitignore ├── LICENSE ├── 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 │ ├── getting-started.md │ ├── index.md │ └── utilities │ │ ├── active-element.md │ │ ├── animation-frames.md │ │ ├── context.md │ │ ├── debounced.md │ │ ├── element-rect.md │ │ ├── element-size.md │ │ ├── extract.md │ │ ├── finite-state-machine.md │ │ ├── is-focus-within.md │ │ ├── is-idle.md │ │ ├── is-in-viewport.md │ │ ├── is-mounted.md │ │ ├── on-click-outside.md │ │ ├── persisted-state.md │ │ ├── pressed-keys.md │ │ ├── previous.md │ │ ├── resource.md │ │ ├── scroll-state.md │ │ ├── state-history.md │ │ ├── textarea-autosize.md │ │ ├── use-debounce.md │ │ ├── use-event-listener.md │ │ ├── use-geolocation.md │ │ ├── use-intersection-observer.md │ │ ├── use-mutation-observer.md │ │ ├── use-resize-observer.md │ │ └── watch.md ├── lib │ ├── components │ │ ├── blueprint.svelte │ │ ├── demo-note.svelte │ │ ├── demos │ │ │ ├── active-element.svelte │ │ │ ├── animation-frames.svelte │ │ │ ├── debounced.svelte │ │ │ ├── element-rect.svelte │ │ │ ├── element-size.svelte │ │ │ ├── finite-state-machine.svelte │ │ │ ├── is-focus-within.svelte │ │ │ ├── is-idle.svelte │ │ │ ├── is-in-viewport.svelte │ │ │ ├── is-mounted.svelte │ │ │ ├── on-click-outside-dialog.svelte │ │ │ ├── on-click-outside.svelte │ │ │ ├── persisted-state.svelte │ │ │ ├── pressed-keys.svelte │ │ │ ├── previous.svelte │ │ │ ├── resource.svelte │ │ │ ├── scroll-state.svelte │ │ │ ├── state-history.svelte │ │ │ ├── textarea-autosize.svelte │ │ │ ├── use-debounce.svelte │ │ │ ├── use-event-listener.svelte │ │ │ ├── use-geolocation.svelte │ │ │ ├── use-intersection-observer.svelte │ │ │ ├── use-mutation-observer.svelte │ │ │ └── use-resize-observer.svelte │ │ └── logos │ │ │ ├── runed-dark.svelte │ │ │ ├── runed-icon.svelte │ │ │ └── runed-light.svelte │ ├── config │ │ ├── navigation.ts │ │ └── site.ts │ ├── index.ts │ └── utils │ │ └── docs.ts └── routes │ ├── (docs) │ ├── +layout.svelte │ ├── +layout.ts │ └── docs │ │ ├── +page.svelte │ │ ├── +page.ts │ │ └── [...slug] │ │ ├── +page.svelte │ │ └── +page.ts │ ├── (landing) │ └── +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 ├── logo-dark.svg ├── logo-light.svg ├── mouse_sprite.png ├── og.png └── site.webmanifest ├── svelte.config.js ├── tsconfig.json ├── velite.config.js └── vite.config.ts /.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.1/schema.json", 3 | "changelog": [ 4 | "@svitejs/changesets-changelog-github-compact", 5 | { "repo": "svecosystem/runed" } 6 | ], 7 | "commit": false, 8 | "fixed": [], 9 | "linked": [], 10 | "access": "public", 11 | "baseBranch": "main", 12 | "updateInternalDependencies": "patch", 13 | "ignore": ["docs"] 14 | } 15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [huntabyte, tglide] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: huntabyte 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 runed 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/runed/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,runed,@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 runed 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/runed/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: sites/docs/.svelte-kit 34 | include-hidden-files: true 35 | -------------------------------------------------------------------------------- /.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: runed 33 | directory: ./.svelte-kit/cloudflare 34 | workingDirectory: sites/docs 35 | deploymentName: Production 36 | wranglerVersion: "" # uses the local version of wrangler 37 | -------------------------------------------------------------------------------- /.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: runed 36 | deploymentName: Preview 37 | directory: ${{ steps.preview-build-artifact.outputs.download-path }}/cloudflare 38 | wranglerVersion: "4" 39 | -------------------------------------------------------------------------------- /.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: runed 57 | directory: ./.svelte-kit/cloudflare 58 | workingDirectory: sites/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 | node_modules 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | dist 7 | 8 | .velite 9 | wrangler/**/* 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | lerna-debug.log* 18 | .pnpm-debug.log* 19 | 20 | # Diagnostic reports (https://nodejs.org/api/report.html) 21 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 22 | 23 | # Runtime data 24 | pids 25 | *.pid 26 | *.seed 27 | *.pid.lock 28 | 29 | # Directory for instrumented libs generated by jscoverage/JSCover 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | coverage 34 | *.lcov 35 | 36 | # nyc test coverage 37 | .nyc_output 38 | 39 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 40 | .grunt 41 | 42 | # Bower dependency directory (https://bower.io/) 43 | bower_components 44 | 45 | # node-waf configuration 46 | .lock-wscript 47 | 48 | # Compiled binary addons (https://nodejs.org/api/addons.html) 49 | build/Release 50 | 51 | # Dependency directories 52 | node_modules/ 53 | jspm_packages/ 54 | 55 | # Snowpack dependency directory (https://snowpack.dev/) 56 | web_modules/ 57 | 58 | # TypeScript cache 59 | *.tsbuildinfo 60 | 61 | # Optional npm cache directory 62 | .npm 63 | 64 | # Optional eslint cache 65 | .eslintcache 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variable files 74 | .env 75 | .env.development.local 76 | .env.test.local 77 | .env.production.local 78 | .env.local 79 | 80 | sites/docs/.vercel 81 | packages/runed/doc 82 | sites/docs/.wrangler 83 | sites/docs/.velite -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | package-manager-strict=false 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.15.1 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | **/build 4 | **/.svelte-kit 5 | .env 6 | .env.* 7 | !.env.example 8 | 9 | # Ignore files for PNPM, NPM and YARN 10 | pnpm-lock.yaml 11 | package-lock.json 12 | yarn.lock 13 | **/.changeset/ 14 | .prettierrc 15 | .vercel 16 | .contentlayer 17 | **/dist 18 | **/.github 19 | CHANGELOG.md 20 | vite.config.js.timestamp-* 21 | vite.config.ts.timestamp-* 22 | sites/docs/src/routes/api/search.json/**/* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": false, 4 | "trailingComma": "es5", 5 | "semi": true, 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 | "files": "*.md", 17 | "options": { 18 | "parser": "markdown", 19 | "printWidth": 100, 20 | "proseWrap": "always", 21 | "useTabs": true, 22 | "trailingComma": "none", 23 | "bracketSameLine": true 24 | } 25 | } 26 | ], 27 | "tailwindFunctions": ["clsx", "cn", "tv"] 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hunter Johnston 4 | Copyright (c) 2024 Thomas G. Lopes 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Runed 2 | 3 | 4 | 5 | [![npm version](https://flat.badgen.net/npm/v/runed?color=green)](https://npmjs.com/package/runed) 6 | [![npm downloads](https://flat.badgen.net/npm/dm/runed?color=green)](https://npmjs.com/package/runed) 7 | [![license](https://flat.badgen.net/github/license/svecosystem/runed?color=green)](https://github.com/svecosystem/runed/blob/main/LICENSE) 8 | 9 | 10 | 11 | Runed provides utilities to power your applications using the magic of 12 | [Svelte Runes](https://svelte.dev/blog/runes). 13 | 14 | ## Installation 15 | 16 | ```bash 17 | npm install runed 18 | ``` 19 | 20 | Check out the [documentation](https://runed.dev) for more information. 21 | 22 | ## Sponsors 23 | 24 | This project is supported by the following beautiful people/organizations: 25 | 26 |

27 | 28 | Logos from Sponsors 29 | 30 |

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

39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from "eslint-config-prettier"; 2 | import js from "@eslint/js"; 3 | import { includeIgnoreFile } from "@eslint/compat"; 4 | import svelte from "eslint-plugin-svelte"; 5 | import globals from "globals"; 6 | import { fileURLToPath } from "node:url"; 7 | import ts from "typescript-eslint"; 8 | const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url)); 9 | 10 | export default ts.config( 11 | includeIgnoreFile(gitignorePath), 12 | js.configs.recommended, 13 | ...ts.configs.recommended, 14 | ...svelte.configs.recommended, 15 | prettier, 16 | ...svelte.configs.prettier, 17 | { 18 | languageOptions: { 19 | globals: { ...globals.browser, ...globals.node }, 20 | }, 21 | rules: { "no-undef": "off" }, 22 | }, 23 | { 24 | files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"], 25 | ignores: ["eslint.config.js", "svelte.config.js"], 26 | languageOptions: { 27 | parserOptions: { 28 | projectService: true, 29 | extraFileExtensions: [".svelte"], 30 | parser: ts.parser, 31 | }, 32 | }, 33 | rules: { 34 | "prefer-const": "off", 35 | }, 36 | }, 37 | { 38 | rules: { 39 | "@typescript-eslint/no-unused-vars": [ 40 | "error", 41 | { 42 | argsIgnorePattern: "^_", 43 | varsIgnorePattern: "^_", 44 | }, 45 | ], 46 | "@typescript-eslint/no-unused-expressions": "off", 47 | "@typescript-eslint/no-empty-object-type": "off", 48 | }, 49 | }, 50 | { 51 | ignores: [ 52 | "build/", 53 | ".svelte-kit/", 54 | "dist/", 55 | ".svelte-kit/**/*", 56 | "sites/docs/.svelte-kit/**/*", 57 | ".svelte-kit", 58 | "packages/runed/dist/**/*", 59 | "packages/runed/.svelte-kit/**/*", 60 | ], 61 | } 62 | ); 63 | -------------------------------------------------------------------------------- /maintainers.md: -------------------------------------------------------------------------------- 1 | # Maintainer's Guide: Shipping & Ownership 2 | 3 | ## Core Philosophy 4 | 5 | At Runed, we believe in empowering maintainers to make meaningful contributions without unnecessary 6 | bureaucracy. This document outlines our approach to shipping features and making decisions. 7 | 8 | ## Performance and Developer Experience 9 | 10 | Finding the right balance between performance and developer experience (DX) is a core responsibility 11 | of every maintainer. We believe that: 12 | 13 | - Neither performance nor DX should be sacrificed entirely for the other 14 | - Every feature should be evaluated through both lenses where applicable 15 | - Performance impacts should be measured, not assumed 16 | - DX improvements should be validated with real-world usage 17 | - Complex performance optimizations must be justified by measurable benefits 18 | - "Developer-friendly" shouldn't and doesn't mean "performance-ignorant" 19 | 20 | When making decisions, consider: 21 | 22 | - Will this make the library easier to use correctly and harder to use incorrectly? 23 | - Does the performance cost justify the DX benefit, or vice versa? 24 | - Can we achieve both good DX and performance through clever API design? 25 | - Are we making assumptions about performance without data to back them up? 26 | 27 | ## Ownership & Decision Making 28 | 29 | As a maintainer, you have full autonomy to ship features, improvements, and fixes that you believe 30 | add value to Runed. What this means in practice is: 31 | 32 | - You don't need explicit permission to start working on or ship something 33 | - Your judgement about what's valuable is trusted 34 | - You own the decisions about your contributions 35 | - Other maintainers can (and should) provide feedback, but you decide whether to act on it 36 | - You're empowered to merge your own PRs when you are confident in them 37 | 38 | ## Shipping Philosophy 39 | 40 | Ship early and ship often. As a maintainer, you're empowered to move quickly and make decisions. 41 | What this means in practice is: 42 | 43 | - Bug fixes should be released immediately - users shouldn't wait for fixes we've already made just 44 | to reduce the number of patch releases 45 | - Breaking changes need proper major version bumps and documentation, but not necessarily consensus. 46 | Use your best judgement if a change is worth breaking compatibility 47 | - Consider the impact on users when making breaking changes, but don't let that paralyze you 48 | - New features can be shipping when you believe they're ready 49 | - Experiment freely with new approaches - we can always iterate based on feedback 50 | 51 | Remember: It's often better to ship something good now than to wait for something perfect later. 52 | 53 | ## Best Practices 54 | 55 | To maintain a healthy codebase and team dynamic, we should strive to follow these best practices: 56 | 57 | ### Document Your Changes 58 | 59 | - Write clear and concise PR descriptions 60 | - Update the relevant documentation 61 | - Add inline comments for non-obvious code 62 | 63 | ### Maintain Quality 64 | 65 | - Write tests for new functionality 66 | - Ensure CI passes 67 | - Consider performance implications 68 | 69 | ### Communicate 70 | 71 | - Use PR descriptions to explain your reasoning 72 | - Tag relevant maintainers for awareness 73 | - Respond to feedback, even if you decide not to act on it (it's okay to disagree) 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "description": "Monorepo for Runed.", 4 | "private": true, 5 | "version": "0.0.0", 6 | "homepage": "https://runed.dev", 7 | "contributors": [ 8 | { 9 | "name": "Thomas G. Lopes", 10 | "url": "https://thomasglopes.com" 11 | }, 12 | { 13 | "name": "Hunter Johnston", 14 | "url": "https://x.com/huntabyte" 15 | } 16 | ], 17 | "funding": [ 18 | "https://github.com/sponsors/huntabyte", 19 | "https://github.com/sponsors/tglide" 20 | ], 21 | "main": "index.js", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/svecosystem/runed" 25 | }, 26 | "scripts": { 27 | "dev": "pnpm sync && pnpm --parallel dev", 28 | "test": "pnpm -r test", 29 | "test:package": "pnpm -F \"./packages/**\" test", 30 | "test:package:watch": "pnpm -F \"./packages/**\" test:watch", 31 | "build": "pnpm -r build", 32 | "build:packages": "pnpm -F \"./packages/**\" --parallel build", 33 | "build:content": "pnpm -F \"./sites/**\" --parallel build:content", 34 | "ci:publish": "pnpm build:packages && changeset publish", 35 | "lint": "prettier --check . && eslint .", 36 | "lint:fix": "prettier --write . && eslint . --fix", 37 | "format": "prettier --write .", 38 | "check": "pnpm -r check", 39 | "sync": "pnpm -r sync", 40 | "add": "node ./scripts/add-utility.mjs" 41 | }, 42 | "license": "MIT", 43 | "devDependencies": { 44 | "@changesets/cli": "^2.27.10", 45 | "@eslint/compat": "^1.2.8", 46 | "@eslint/js": "^9.18.0", 47 | "@svitejs/changesets-changelog-github-compact": "^1.2.0", 48 | "eslint": "^9.18.0", 49 | "eslint-config-prettier": "^10.0.1", 50 | "eslint-plugin-svelte": "^3.5.0", 51 | "globals": "^16.0.0", 52 | "prettier": "^3.3.3", 53 | "prettier-plugin-svelte": "^3.3.3", 54 | "prettier-plugin-tailwindcss": "^0.6.11", 55 | "readline-sync": "^1.4.10", 56 | "svelte": "^5.11.0", 57 | "typescript": "^5.6.3", 58 | "typescript-eslint": "^8.20.0", 59 | "wrangler": "^4.15.0" 60 | }, 61 | "type": "module", 62 | "engines": { 63 | "pnpm": ">=9.0.0", 64 | "node": ">=18" 65 | }, 66 | "packageManager": "pnpm@9.14.4" 67 | } 68 | -------------------------------------------------------------------------------- /packages/runed/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /dist 5 | /.svelte-kit 6 | /package 7 | .env 8 | .env.* 9 | !.env.example 10 | vite.config.js.timestamp-* 11 | vite.config.ts.timestamp-* 12 | -------------------------------------------------------------------------------- /packages/runed/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hunter Johnston 4 | Copyright (c) 2024 Thomas G. Lopes 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /packages/runed/README.md: -------------------------------------------------------------------------------- 1 | # Runed 2 | 3 | 4 | 5 | [![npm version](https://flat.badgen.net/npm/v/runed?color=green)](https://npmjs.com/package/runed) 6 | [![npm downloads](https://flat.badgen.net/npm/dm/runed?color=green)](https://npmjs.com/package/runed) 7 | [![license](https://flat.badgen.net/github/license/svecosystem/runed?color=green)](https://github.com/svecosystem/runed/blob/main/LICENSE) 8 | 9 | 10 | 11 | Runed provides utilities to power your applications using the magic of 12 | [Svelte Runes](https://svelte.dev/blog/runes). 13 | 14 | ## Features 15 | 16 | 17 | 18 | ## Installation 19 | 20 | Runed will be published to NPM once Svelte 5 is released. 21 | 22 | ## License 23 | 24 | 25 | 26 | Published under the [MIT](https://github.com/svecosystem/runed/blob/main/LICENSE) license. Made by 27 | [@tglide](https://github.com/tglide), [@huntabyte](https://github.com/huntabyte) and 28 | [community](https://github.com/svecosystem/runed/graphs/contributors) 💛

29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /packages/runed/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "runed", 3 | "version": "0.28.0", 4 | "type": "module", 5 | "svelte": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "license": "MIT", 8 | "contributors": [ 9 | { 10 | "name": "Thomas G. Lopes", 11 | "url": "https://thomasglopes.com" 12 | }, 13 | { 14 | "name": "Hunter Johnston", 15 | "url": "https://x.com/huntabyte" 16 | } 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/svecosystem/runed", 21 | "directory": "packages/runed" 22 | }, 23 | "funding": [ 24 | "https://github.com/sponsors/huntabyte", 25 | "https://github.com/sponsors/tglide" 26 | ], 27 | "scripts": { 28 | "dev": "pnpm sync && pnpm watch", 29 | "build": "pnpm package", 30 | "package": "svelte-kit sync && svelte-package && publint", 31 | "test": "vitest --run", 32 | "test:watch": "vitest --watch", 33 | "test:ui": "vitest --watch --ui", 34 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 35 | "watch": "svelte-kit sync && svelte-package --watch" 36 | }, 37 | "exports": { 38 | ".": { 39 | "types": "./dist/index.d.ts", 40 | "svelte": "./dist/index.js" 41 | } 42 | }, 43 | "files": [ 44 | "dist", 45 | "!dist/**/*.test.*", 46 | "!dist/**/*.spec.*" 47 | ], 48 | "peerDependencies": { 49 | "svelte": "^5.7.0" 50 | }, 51 | "devDependencies": { 52 | "@sveltejs/kit": "^2.5.3", 53 | "@sveltejs/package": "^2.3.0", 54 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 55 | "@testing-library/dom": "^10.2.0", 56 | "@testing-library/jest-dom": "^6.4.6", 57 | "@testing-library/svelte": "^5.2.0", 58 | "@testing-library/user-event": "^14.5.2", 59 | "@types/node": "^20.10.6", 60 | "@vitest/browser": "^3.1.3", 61 | "@vitest/coverage-v8": "^3.1.3", 62 | "@vitest/ui": "^3.1.3", 63 | "jsdom": "^24.0.0", 64 | "msw": "^2.7.0", 65 | "playwright": "^1.52.0", 66 | "publint": "^0.1.9", 67 | "resize-observer-polyfill": "^1.5.1", 68 | "svelte": "^5.28.6", 69 | "svelte-check": "^4.1.1", 70 | "tslib": "^2.4.1", 71 | "typescript": "^5.0.0", 72 | "vite": "^6.3.5", 73 | "vitest": "^3.1.3" 74 | }, 75 | "dependencies": { 76 | "esm-env": "^1.0.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/runed/setupTest.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/svelte/vitest"; 2 | import "@testing-library/jest-dom/vitest"; 3 | import { vi } from "vitest"; 4 | import type { Navigation, Page } from "@sveltejs/kit"; 5 | import { readable } from "svelte/store"; 6 | import type * as environment from "$app/environment"; 7 | import type * as navigation from "$app/navigation"; 8 | import type * as stores from "$app/stores"; 9 | 10 | // Mock SvelteKit runtime module $app/environment 11 | vi.mock("$app/environment", (): typeof environment => ({ 12 | browser: false, 13 | dev: true, 14 | building: false, 15 | version: "any", 16 | })); 17 | 18 | // Mock SvelteKit runtime module $app/navigation 19 | vi.mock("$app/navigation", (): typeof navigation => ({ 20 | afterNavigate: () => {}, 21 | beforeNavigate: () => {}, 22 | disableScrollHandling: () => {}, 23 | goto: () => Promise.resolve(), 24 | invalidate: () => Promise.resolve(), 25 | invalidateAll: () => Promise.resolve(), 26 | preloadData: () => Promise.resolve({ type: "loaded" as const, status: 200, data: {} }), 27 | preloadCode: () => Promise.resolve(), 28 | onNavigate: () => {}, 29 | pushState: () => {}, 30 | replaceState: () => {}, 31 | })); 32 | 33 | // Mock SvelteKit runtime module $app/stores 34 | vi.mock("$app/stores", (): typeof stores => { 35 | const getStores: typeof stores.getStores = () => { 36 | const navigating = readable(null); 37 | const page = readable({ 38 | url: new URL("http://localhost"), 39 | params: {}, 40 | route: { 41 | id: null, 42 | }, 43 | status: 200, 44 | error: null, 45 | data: {}, 46 | form: undefined, 47 | state: {}, 48 | }); 49 | const updated = { subscribe: readable(false).subscribe, check: async () => false }; 50 | 51 | return { navigating, page, updated }; 52 | }; 53 | 54 | const page: typeof stores.page = { 55 | subscribe(fn) { 56 | return getStores().page.subscribe(fn); 57 | }, 58 | }; 59 | const navigating: typeof stores.navigating = { 60 | subscribe(fn) { 61 | return getStores().navigating.subscribe(fn); 62 | }, 63 | }; 64 | const updated: typeof stores.updated = { 65 | subscribe(fn) { 66 | return getStores().updated.subscribe(fn); 67 | }, 68 | check: async () => false, 69 | }; 70 | 71 | return { 72 | getStores, 73 | navigating, 74 | page, 75 | updated, 76 | }; 77 | }); 78 | 79 | // eslint-disable-next-line @typescript-eslint/no-require-imports 80 | globalThis.ResizeObserver = require("resize-observer-polyfill"); 81 | Element.prototype.scrollIntoView = () => {}; 82 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 83 | Element.prototype.hasPointerCapture = (() => {}) as any; 84 | 85 | // @ts-expect-error - shut it 86 | globalThis.window.CSS.supports = (_property: string, _value: string) => true; 87 | 88 | globalThis.document.elementsFromPoint = () => []; 89 | globalThis.document.elementFromPoint = () => null; 90 | -------------------------------------------------------------------------------- /packages/runed/src/app.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace App { 3 | // interface Error {} 4 | // interface Locals {} 5 | // interface PageData {} 6 | // interface PageState {} 7 | // interface Platform {} 8 | } 9 | } 10 | 11 | export {}; 12 | -------------------------------------------------------------------------------- /packages/runed/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/runed/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | describe("sum test", () => { 4 | it("adds 1 + 2 to equal 3", () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/runed/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./utilities/index.js"; 2 | export type { MaybeGetter, Getter, Setter } from "./internal/types.js"; 3 | -------------------------------------------------------------------------------- /packages/runed/src/lib/internal/configurable-globals.ts: -------------------------------------------------------------------------------- 1 | import { BROWSER } from "esm-env"; 2 | 3 | export type ConfigurableWindow = { 4 | /** Provide a custom `window` object to use in place of the global `window` object. */ 5 | window?: typeof globalThis & Window; 6 | }; 7 | 8 | export type ConfigurableDocument = { 9 | /** Provide a custom `document` object to use in place of the global `document` object. */ 10 | document?: Document; 11 | }; 12 | 13 | export type ConfigurableDocumentOrShadowRoot = { 14 | /* 15 | * Specify a custom `document` instance or a shadow root, e.g. working with iframes or in testing environments. 16 | */ 17 | document?: DocumentOrShadowRoot; 18 | }; 19 | 20 | export type ConfigurableNavigator = { 21 | /** Provide a custom `navigator` object to use in place of the global `navigator` object. */ 22 | navigator?: Navigator; 23 | }; 24 | 25 | export type ConfigurableLocation = { 26 | /** Provide a custom `location` object to use in place of the global `location` object. */ 27 | location?: Location; 28 | }; 29 | 30 | export const defaultWindow = BROWSER && typeof window !== "undefined" ? window : undefined; 31 | export const defaultDocument = 32 | BROWSER && typeof window !== "undefined" ? window.document : undefined; 33 | export const defaultNavigator = 34 | BROWSER && typeof window !== "undefined" ? window.navigator : undefined; 35 | export const defaultLocation = 36 | BROWSER && typeof window !== "undefined" ? window.location : undefined; 37 | -------------------------------------------------------------------------------- /packages/runed/src/lib/internal/types.ts: -------------------------------------------------------------------------------- 1 | export type Getter = () => T; 2 | export type MaybeGetter = T | Getter; 3 | export type MaybeElementGetter = MaybeGetter; 4 | export type MaybeElement = HTMLElement | SVGElement | undefined | null; 5 | 6 | export type Setter = (value: T) => void; 7 | export type Expand = T extends infer U ? { [K in keyof U]: U[K] } : never; 8 | export type WritableProperties = { 9 | -readonly [P in keyof T]: T[P]; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/runed/src/lib/internal/utils/array.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get nth item of Array. Negative for backward 3 | */ 4 | export function at(array: readonly T[], index: number): T | undefined { 5 | const len = array.length; 6 | if (!len) return undefined; 7 | 8 | if (index < 0) index += len; 9 | 10 | return array[index]; 11 | } 12 | 13 | export function last(array: readonly T[]): T | undefined { 14 | return array[array.length - 1]; 15 | } 16 | -------------------------------------------------------------------------------- /packages/runed/src/lib/internal/utils/browser.ts: -------------------------------------------------------------------------------- 1 | export { BROWSER as browser } from "esm-env"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/internal/utils/dom.ts: -------------------------------------------------------------------------------- 1 | import { defaultDocument } from "../configurable-globals.js"; 2 | 3 | /** 4 | * Handles getting the active element in a document or shadow root. 5 | * If the active element is within a shadow root, it will traverse the shadow root 6 | * to find the active element. 7 | * If not, it will return the active element in the document. 8 | * 9 | * @param document A document or shadow root to get the active element from. 10 | * @returns The active element in the document or shadow root. 11 | */ 12 | export function getActiveElement(document: DocumentOrShadowRoot): Element | null { 13 | let activeElement = document.activeElement; 14 | 15 | while (activeElement?.shadowRoot) { 16 | const node = activeElement.shadowRoot.activeElement; 17 | if (node === activeElement) break; 18 | else activeElement = node; 19 | } 20 | 21 | return activeElement; 22 | } 23 | 24 | /** 25 | * Returns the owner document of a given element. 26 | * 27 | * @param node The element to get the owner document from. 28 | * @returns 29 | */ 30 | export function getOwnerDocument( 31 | node: Element | null | undefined, 32 | fallback = defaultDocument 33 | ): Document | undefined { 34 | return node?.ownerDocument ?? fallback; 35 | } 36 | 37 | /** 38 | * Checks if an element is or is contained by another element. 39 | * 40 | * @param node The element to check if it or its descendants contain the target element. 41 | * @param target The element to check if it is contained by the node. 42 | * @returns 43 | */ 44 | export function isOrContainsTarget(node: Element, target: Element) { 45 | return node === target || node.contains(target); 46 | } 47 | -------------------------------------------------------------------------------- /packages/runed/src/lib/internal/utils/function.ts: -------------------------------------------------------------------------------- 1 | export function noop(): void {} 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/internal/utils/get.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeGetter } from "../types.js"; 2 | import { isFunction } from "./is.js"; 3 | 4 | export function get(value: MaybeGetter): T { 5 | if (isFunction(value)) { 6 | return value(); 7 | } 8 | 9 | return value; 10 | } 11 | -------------------------------------------------------------------------------- /packages/runed/src/lib/internal/utils/is.ts: -------------------------------------------------------------------------------- 1 | export function isFunction(value: unknown): value is (...args: unknown[]) => unknown { 2 | return typeof value === "function"; 3 | } 4 | 5 | export function isObject(value: unknown): value is Record { 6 | return value !== null && typeof value === "object"; 7 | } 8 | 9 | export function isElement(value: unknown): value is Element { 10 | return value instanceof Element; 11 | } 12 | -------------------------------------------------------------------------------- /packages/runed/src/lib/internal/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | export async function sleep(ms = 0): Promise { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | } 4 | -------------------------------------------------------------------------------- /packages/runed/src/lib/test/util.svelte.ts: -------------------------------------------------------------------------------- 1 | import { test, vi } from "vitest"; 2 | 3 | export function testWithEffect(name: string, fn: () => void | Promise): void { 4 | test(name, () => effectRootScope(fn)); 5 | } 6 | 7 | export function effectRootScope(fn: () => void | Promise): void | Promise { 8 | let promise!: void | Promise; 9 | const cleanup = $effect.root(() => { 10 | promise = fn(); 11 | }); 12 | 13 | if (promise instanceof Promise) { 14 | return promise.finally(cleanup); 15 | } else { 16 | cleanup(); 17 | } 18 | } 19 | 20 | export function vitestSetTimeoutWrapper(fn: () => void, timeout: number): void { 21 | setTimeout(() => { 22 | fn(); 23 | }, timeout + 1); 24 | 25 | vi.advanceTimersByTime(timeout); 26 | } 27 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/active-element/active-element.svelte.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultWindow, 3 | type ConfigurableDocumentOrShadowRoot, 4 | type ConfigurableWindow, 5 | } from "$lib/internal/configurable-globals.js"; 6 | import { getActiveElement } from "$lib/internal/utils/dom.js"; 7 | import { on } from "svelte/events"; 8 | import { createSubscriber } from "svelte/reactivity"; 9 | 10 | export interface ActiveElementOptions 11 | extends ConfigurableDocumentOrShadowRoot, 12 | ConfigurableWindow {} 13 | 14 | export class ActiveElement { 15 | readonly #document?: DocumentOrShadowRoot; 16 | readonly #subscribe?: () => void; 17 | 18 | constructor(options: ActiveElementOptions = {}) { 19 | const { window = defaultWindow, document = window?.document } = options; 20 | if (window === undefined) return; 21 | 22 | this.#document = document; 23 | this.#subscribe = createSubscriber((update) => { 24 | const cleanupFocusIn = on(window, "focusin", update); 25 | const cleanupFocusOut = on(window, "focusout", update); 26 | return () => { 27 | cleanupFocusIn(); 28 | cleanupFocusOut(); 29 | }; 30 | }); 31 | } 32 | 33 | get current(): Element | null { 34 | this.#subscribe?.(); 35 | if (!this.#document) return null; 36 | return getActiveElement(this.#document); 37 | } 38 | } 39 | 40 | /** 41 | * An object holding a reactive value that is equal to `document.activeElement`. 42 | * It automatically listens for changes, keeping the reference up to date. 43 | * 44 | * If you wish to use a custom document or shadowRoot, you should use 45 | * [useActiveElement](https://runed.dev/docs/utilities/active-element) instead. 46 | * 47 | * @see {@link https://runed.dev/docs/utilities/active-element} 48 | */ 49 | export const activeElement = new ActiveElement(); 50 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/active-element/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./active-element.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/animation-frames/animation-frames.svelte.ts: -------------------------------------------------------------------------------- 1 | import { untrack } from "svelte"; 2 | import { extract } from "../extract/index.js"; 3 | import type { MaybeGetter } from "$lib/internal/types.js"; 4 | import { defaultWindow, type ConfigurableWindow } from "$lib/internal/configurable-globals.js"; 5 | 6 | type RafCallbackParams = { 7 | /** The number of milliseconds since the last frame. */ 8 | delta: number; 9 | /** 10 | * Time elapsed since the creation of the web page. 11 | * See {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#the_time_origin Time origin}. 12 | */ 13 | timestamp: DOMHighResTimeStamp; 14 | }; 15 | 16 | export type AnimationFramesOptions = ConfigurableWindow & { 17 | /** 18 | * Start calling requestAnimationFrame immediately. 19 | * 20 | * @default true 21 | */ 22 | immediate?: boolean; 23 | 24 | /** 25 | * Limit the number of frames per second. 26 | * Set to `0` to disable 27 | * 28 | * @default 0 29 | */ 30 | fpsLimit?: MaybeGetter; 31 | }; 32 | 33 | /** 34 | * Wrapper over {@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame requestAnimationFrame}, 35 | * with controls for pausing and resuming the animation, reactive tracking and optional limiting of fps, and utilities. 36 | */ 37 | export class AnimationFrames { 38 | #callback: (params: RafCallbackParams) => void; 39 | #fpsLimitOption: AnimationFramesOptions["fpsLimit"] = 0; 40 | #fpsLimit = $derived(extract(this.#fpsLimitOption) ?? 0); 41 | #previousTimestamp: number | null = null; 42 | #frame: number | null = null; 43 | #fps = $state(0); 44 | #running = $state(false); 45 | #window = defaultWindow; 46 | 47 | constructor(callback: (params: RafCallbackParams) => void, options: AnimationFramesOptions = {}) { 48 | if (options.window) this.#window = options.window; 49 | this.#fpsLimitOption = options.fpsLimit; 50 | this.#callback = callback; 51 | 52 | this.start = this.start.bind(this); 53 | this.stop = this.stop.bind(this); 54 | this.toggle = this.toggle.bind(this); 55 | 56 | $effect(() => { 57 | if (options.immediate ?? true) { 58 | untrack(this.start); 59 | } 60 | 61 | return this.stop; 62 | }); 63 | } 64 | 65 | #loop(timestamp: DOMHighResTimeStamp): void { 66 | if (!this.#running || !this.#window) return; 67 | 68 | if (this.#previousTimestamp === null) { 69 | this.#previousTimestamp = timestamp; 70 | } 71 | 72 | const delta = timestamp - this.#previousTimestamp; 73 | const fps = 1000 / delta; 74 | if (this.#fpsLimit && fps > this.#fpsLimit) { 75 | this.#frame = this.#window.requestAnimationFrame(this.#loop.bind(this)); 76 | return; 77 | } 78 | 79 | this.#fps = fps; 80 | this.#previousTimestamp = timestamp; 81 | this.#callback({ delta, timestamp }); 82 | this.#frame = this.#window.requestAnimationFrame(this.#loop.bind(this)); 83 | } 84 | 85 | start(): void { 86 | if (!this.#window) return; 87 | this.#running = true; 88 | this.#previousTimestamp = 0; 89 | this.#frame = this.#window.requestAnimationFrame(this.#loop.bind(this)); 90 | } 91 | 92 | stop(): void { 93 | if (!this.#frame || !this.#window) return; 94 | this.#running = false; 95 | this.#window.cancelAnimationFrame(this.#frame); 96 | this.#frame = null; 97 | } 98 | 99 | toggle(): void { 100 | this.#running ? this.stop() : this.start(); 101 | } 102 | 103 | get fps(): number { 104 | return !this.#running ? 0 : this.#fps; 105 | } 106 | 107 | get running(): boolean { 108 | return this.#running; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/animation-frames/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./animation-frames.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/context/context.ts: -------------------------------------------------------------------------------- 1 | import { getContext, hasContext, setContext } from "svelte"; 2 | 3 | export class Context { 4 | readonly #name: string; 5 | readonly #key: symbol; 6 | 7 | /** 8 | * @param name The name of the context. 9 | * This is used for generating the context key and error messages. 10 | */ 11 | constructor(name: string) { 12 | this.#name = name; 13 | this.#key = Symbol(name); 14 | } 15 | 16 | /** 17 | * The key used to get and set the context. 18 | * 19 | * It is not recommended to use this value directly. 20 | * Instead, use the methods provided by this class. 21 | */ 22 | get key(): symbol { 23 | return this.#key; 24 | } 25 | 26 | /** 27 | * Checks whether this has been set in the context of a parent component. 28 | * 29 | * Must be called during component initialisation. 30 | */ 31 | exists(): boolean { 32 | return hasContext(this.#key); 33 | } 34 | 35 | /** 36 | * Retrieves the context that belongs to the closest parent component. 37 | * 38 | * Must be called during component initialisation. 39 | * 40 | * @throws An error if the context does not exist. 41 | */ 42 | get(): TContext { 43 | const context: TContext | undefined = getContext(this.#key); 44 | if (context === undefined) { 45 | throw new Error(`Context "${this.#name}" not found`); 46 | } 47 | return context; 48 | } 49 | 50 | /** 51 | * Retrieves the context that belongs to the closest parent component, 52 | * or the given fallback value if the context does not exist. 53 | * 54 | * Must be called during component initialisation. 55 | */ 56 | getOr(fallback: TFallback): TContext | TFallback { 57 | const context: TContext | undefined = getContext(this.#key); 58 | if (context === undefined) { 59 | return fallback; 60 | } 61 | return context; 62 | } 63 | 64 | /** 65 | * Associates the given value with the current component and returns it. 66 | * 67 | * Must be called during component initialisation. 68 | */ 69 | set(context: TContext): TContext { 70 | return setContext(this.#key, context); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/context/index.ts: -------------------------------------------------------------------------------- 1 | export { Context } from "./context.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/debounced/debounced.svelte.ts: -------------------------------------------------------------------------------- 1 | import { useDebounce } from "../use-debounce/use-debounce.svelte.js"; 2 | import { watch } from "../watch/watch.svelte.js"; 3 | import type { Getter, MaybeGetter } from "$lib/internal/types.js"; 4 | import { noop } from "$lib/internal/utils/function.js"; 5 | 6 | /** 7 | * A wrapper over {@link useDebounce} that creates a debounced state. 8 | * It takes a "getter" function which returns the state you want to debounce. 9 | * Every time this state changes a timer (re)starts, the length of which is 10 | * configurable with the `wait` arg. When the timer ends the `current` value 11 | * is updated. 12 | * 13 | * @see https://runed.dev/docs/utilities/debounced 14 | * 15 | * @example 16 | * 17 | * 23 | * 24 | *
25 | * 26 | *

You searched for: {debounced.current}

27 | *
28 | */ 29 | export class Debounced { 30 | #current: T = $state()!; 31 | #debounceFn: ReturnType; 32 | 33 | /** 34 | * @param getter A function that returns the state to watch. 35 | * @param wait The length of time to wait in ms, defaults to 250. 36 | */ 37 | constructor(getter: Getter, wait: MaybeGetter = 250) { 38 | this.#current = getter(); // immediately set the initial value 39 | this.cancel = this.cancel.bind(this); 40 | this.setImmediately = this.setImmediately.bind(this); 41 | this.updateImmediately = this.updateImmediately.bind(this); 42 | 43 | this.#debounceFn = useDebounce(() => { 44 | this.#current = getter(); 45 | }, wait); 46 | 47 | watch(getter, () => { 48 | this.#debounceFn().catch(noop); 49 | }); 50 | } 51 | 52 | /** 53 | * Get the current value. 54 | */ 55 | get current(): T { 56 | return this.#current; 57 | } 58 | 59 | /** 60 | * Cancel the latest timer. 61 | */ 62 | cancel(): void { 63 | this.#debounceFn.cancel(); 64 | } 65 | 66 | /** 67 | * Run the debounced function immediately. 68 | */ 69 | updateImmediately(): Promise { 70 | return this.#debounceFn.runScheduledNow(); 71 | } 72 | 73 | /** 74 | * Set the `current` value without waiting. 75 | */ 76 | setImmediately(v: T): void { 77 | this.cancel(); 78 | this.#current = v; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/debounced/debounced.test.svelte.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect } from "vitest"; 2 | import { Debounced } from "./index.js"; 3 | import { testWithEffect } from "$lib/test/util.svelte.js"; 4 | 5 | describe("Debounced", () => { 6 | testWithEffect("Value does not get updated immediately", async () => { 7 | let value = $state(0); 8 | const debounced = new Debounced(() => value, 100); 9 | 10 | expect(debounced.current).toBe(0); 11 | value = 1; 12 | expect(debounced.current).toBe(0); 13 | await new Promise((resolve) => setTimeout(resolve, 200)); 14 | expect(debounced.current).toBe(1); 15 | }); 16 | 17 | testWithEffect("Can cancel debounced update", async () => { 18 | let value = $state(0); 19 | const debounced = new Debounced(() => value, 100); 20 | 21 | expect(debounced.current).toBe(0); 22 | value = 1; 23 | expect(debounced.current).toBe(0); 24 | debounced.cancel(); 25 | await new Promise((resolve) => setTimeout(resolve, 200)); 26 | expect(debounced.current).toBe(0); 27 | }); 28 | 29 | testWithEffect("Can set value immediately", async () => { 30 | let value = $state(0); 31 | const debounced = new Debounced(() => value, 100); 32 | 33 | expect(debounced.current).toBe(0); 34 | value = 1; 35 | expect(debounced.current).toBe(0); 36 | await new Promise((resolve) => setTimeout(resolve, 200)); 37 | expect(debounced.current).toBe(1); 38 | 39 | debounced.setImmediately(2); 40 | expect(debounced.current).toBe(2); 41 | }); 42 | 43 | testWithEffect("Can run update immediately", async () => { 44 | let value = $state(0); 45 | const debounced = new Debounced(() => value * 2, 100); 46 | 47 | expect(debounced.current).toBe(0); 48 | value = 1; 49 | expect(debounced.current).toBe(0); 50 | await debounced.updateImmediately(); 51 | expect(debounced.current).toBe(2); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/debounced/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./debounced.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/element-rect/element-rect.svelte.ts: -------------------------------------------------------------------------------- 1 | import { extract } from "../extract/extract.svelte.js"; 2 | import { useMutationObserver } from "../use-mutation-observer/use-mutation-observer.svelte.js"; 3 | import { useResizeObserver } from "../use-resize-observer/use-resize-observer.svelte.js"; 4 | import type { MaybeElementGetter, WritableProperties } from "$lib/internal/types.js"; 5 | import type { ConfigurableWindow } from "$lib/internal/configurable-globals.js"; 6 | 7 | type Rect = WritableProperties>; 8 | 9 | export type ElementRectOptions = ConfigurableWindow & { 10 | initialRect?: DOMRect; 11 | }; 12 | 13 | /** 14 | * Returns a reactive value holding the size of `node`. 15 | * 16 | * Accepts an `options` object with the following properties: 17 | * - `initialSize`: The initial size of the element. Defaults to `{ width: 0, height: 0 }`. 18 | * - `box`: The box model to use. Can be either `"content-box"` or `"border-box"`. Defaults to `"border-box"`. 19 | * 20 | * @returns an object with `width` and `height` properties. 21 | * 22 | * @see {@link https://runed.dev/docs/utilities/element-size} 23 | */ 24 | export class ElementRect { 25 | #rect: Rect = $state({ 26 | x: 0, 27 | y: 0, 28 | width: 0, 29 | height: 0, 30 | top: 0, 31 | right: 0, 32 | bottom: 0, 33 | left: 0, 34 | }); 35 | 36 | constructor(node: MaybeElementGetter, options: ElementRectOptions = {}) { 37 | this.#rect = { 38 | width: options.initialRect?.width ?? 0, 39 | height: options.initialRect?.height ?? 0, 40 | x: options.initialRect?.x ?? 0, 41 | y: options.initialRect?.y ?? 0, 42 | top: options.initialRect?.top ?? 0, 43 | right: options.initialRect?.right ?? 0, 44 | bottom: options.initialRect?.bottom ?? 0, 45 | left: options.initialRect?.left ?? 0, 46 | }; 47 | 48 | const el = $derived(extract(node)); 49 | const update = () => { 50 | if (!el) return; 51 | const rect = el.getBoundingClientRect(); 52 | this.#rect.width = rect.width; 53 | this.#rect.height = rect.height; 54 | this.#rect.x = rect.x; 55 | this.#rect.y = rect.y; 56 | this.#rect.top = rect.top; 57 | this.#rect.right = rect.right; 58 | this.#rect.bottom = rect.bottom; 59 | this.#rect.left = rect.left; 60 | }; 61 | 62 | useResizeObserver(() => el, update, { window: options.window }); 63 | $effect(update); 64 | useMutationObserver(() => el, update, { 65 | attributeFilter: ["style", "class"], 66 | window: options.window, 67 | }); 68 | } 69 | 70 | get x(): number { 71 | return this.#rect.x; 72 | } 73 | 74 | get y(): number { 75 | return this.#rect.y; 76 | } 77 | 78 | get width(): number { 79 | return this.#rect.width; 80 | } 81 | 82 | get height(): number { 83 | return this.#rect.height; 84 | } 85 | 86 | get top(): number { 87 | return this.#rect.top; 88 | } 89 | 90 | get right(): number { 91 | return this.#rect.right; 92 | } 93 | 94 | get bottom(): number { 95 | return this.#rect.bottom; 96 | } 97 | 98 | get left(): number { 99 | return this.#rect.left; 100 | } 101 | 102 | get current(): Rect { 103 | return this.#rect; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/element-rect/index.ts: -------------------------------------------------------------------------------- 1 | export { ElementRect } from "./element-rect.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/element-size/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./element-size.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/extract/extract.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeGetter } from "$lib/internal/types.js"; 2 | import { isFunction } from "$lib/internal/utils/is.js"; 3 | 4 | /** 5 | * Resolves a value that may be a getter function or a direct value. 6 | * 7 | * If the input is a function, it will be invoked to retrieve the actual value. 8 | * If the resolved value (or the input itself) is `undefined`, the optional 9 | * `defaultValue` is returned instead. 10 | * 11 | * @template T - The expected return type. 12 | * @param value - A value or a function that returns a value. 13 | * @param defaultValue - A fallback value returned if the resolved value is `undefined`. 14 | * @returns The resolved value or the default. 15 | */ 16 | export function extract(value: MaybeGetter): T; 17 | export function extract(value: MaybeGetter, defaultValue: T): T; 18 | 19 | export function extract(value: unknown, defaultValue?: unknown) { 20 | if (isFunction(value)) { 21 | const getter = value; 22 | const gotten = getter(); 23 | if (gotten === undefined) return defaultValue; 24 | return gotten; 25 | } 26 | 27 | if (value === undefined) return defaultValue; 28 | return value; 29 | } 30 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/extract/index.ts: -------------------------------------------------------------------------------- 1 | export { extract } from "./extract.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/finite-state-machine/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./finite-state-machine.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./active-element/index.js"; 2 | export * from "./animation-frames/index.js"; 3 | export * from "./context/index.js"; 4 | export * from "./debounced/index.js"; 5 | export * from "./element-rect/index.js"; 6 | export * from "./element-size/index.js"; 7 | export * from "./extract/index.js"; 8 | export * from "./finite-state-machine/index.js"; 9 | export * from "./is-focus-within/index.js"; 10 | export * from "./is-idle/index.js"; 11 | export * from "./is-in-viewport/index.js"; 12 | export * from "./is-mounted/index.js"; 13 | export * from "./on-click-outside/index.js"; 14 | export * from "./persisted-state/index.js"; 15 | export * from "./pressed-keys/index.js"; 16 | export * from "./previous/index.js"; 17 | export * from "./resource/index.js"; 18 | export * from "./state-history/index.js"; 19 | export * from "./textarea-autosize/index.js"; 20 | export * from "./use-debounce/index.js"; 21 | export * from "./use-event-listener/index.js"; 22 | export * from "./use-geolocation/index.js"; 23 | export * from "./use-intersection-observer/index.js"; 24 | export * from "./use-mutation-observer/index.js"; 25 | export * from "./use-resize-observer/index.js"; 26 | export * from "./watch/index.js"; 27 | 28 | export * from "./scroll-state/index.js"; 29 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/is-focus-within/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./is-focus-within.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/is-focus-within/is-focus-within.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeElementGetter } from "$lib/internal/types.js"; 2 | import { 3 | ActiveElement, 4 | type ActiveElementOptions, 5 | } from "../active-element/active-element.svelte.js"; 6 | import { extract } from "../extract/extract.svelte.js"; 7 | 8 | export interface IsFocusWithinOptions extends ActiveElementOptions {} 9 | 10 | /** 11 | * Tracks whether the focus is within a target element. 12 | * @see {@link https://runed.dev/docs/utilities/is-focus-within} 13 | */ 14 | export class IsFocusWithin { 15 | readonly #node: MaybeElementGetter; 16 | readonly #activeElement: ActiveElement; 17 | 18 | constructor(node: MaybeElementGetter, options: IsFocusWithinOptions = {}) { 19 | this.#node = node; 20 | this.#activeElement = new ActiveElement(options); 21 | } 22 | 23 | readonly current = $derived.by(() => { 24 | const node = extract(this.#node); 25 | if (node == null) return false; 26 | return node.contains(this.#activeElement.current); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/is-idle/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./is-idle.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/is-idle/is-idle.svelte.ts: -------------------------------------------------------------------------------- 1 | import { extract } from "../extract/index.js"; 2 | import { useDebounce } from "../use-debounce/index.js"; 3 | import type { MaybeGetter } from "$lib/internal/types.js"; 4 | import { useEventListener } from "$lib/utilities/use-event-listener/use-event-listener.svelte.js"; 5 | import { 6 | defaultWindow, 7 | type ConfigurableDocument, 8 | type ConfigurableWindow, 9 | } from "$lib/internal/configurable-globals.js"; 10 | 11 | type WindowEvent = keyof WindowEventMap; 12 | 13 | export type IsIdleOptions = ConfigurableDocument & 14 | ConfigurableWindow & { 15 | /** 16 | * The events that should set the idle state to `true` 17 | * 18 | * @default ['mousemove', 'mousedown', 'resize', 'keydown', 'touchstart', 'wheel'] 19 | */ 20 | events?: MaybeGetter<(keyof WindowEventMap)[]>; 21 | /** 22 | * The timeout in milliseconds before the idle state is set to `true`. Defaults to 60 seconds. 23 | * 24 | * @default 60000 25 | */ 26 | timeout?: MaybeGetter; 27 | /** 28 | * Detect document visibility changes 29 | * 30 | * @default false 31 | */ 32 | detectVisibilityChanges?: MaybeGetter; 33 | /** 34 | * The initial state of the idle property 35 | * 36 | * @default false 37 | */ 38 | initialState?: boolean; 39 | }; 40 | 41 | const DEFAULT_EVENTS = [ 42 | "keypress", 43 | "mousemove", 44 | "touchmove", 45 | "click", 46 | "scroll", 47 | ] satisfies WindowEvent[]; 48 | 49 | const DEFAULT_OPTIONS = { 50 | events: DEFAULT_EVENTS, 51 | initialState: false, 52 | timeout: 60000, 53 | } satisfies IsIdleOptions; 54 | 55 | /** 56 | * Tracks whether the user is being inactive. 57 | * @see {@link https://runed.dev/docs/utilities/is-idle} 58 | */ 59 | export class IsIdle { 60 | #current: boolean = $state(false); 61 | #lastActive = $state(Date.now()); 62 | 63 | constructor(_options?: IsIdleOptions) { 64 | const opts = { 65 | ...DEFAULT_OPTIONS, 66 | ..._options, 67 | }; 68 | const window = opts.window ?? defaultWindow; 69 | const document = opts.document ?? window?.document; 70 | 71 | const timeout = $derived(extract(opts.timeout)); 72 | const events = $derived(extract(opts.events)); 73 | const detectVisibilityChanges = $derived(extract(opts.detectVisibilityChanges)); 74 | this.#current = opts.initialState; 75 | 76 | const debouncedReset = useDebounce( 77 | () => { 78 | this.#current = true; 79 | }, 80 | () => timeout 81 | ); 82 | 83 | debouncedReset(); 84 | 85 | const handleActivity = () => { 86 | this.#current = false; 87 | this.#lastActive = Date.now(); 88 | debouncedReset(); 89 | }; 90 | 91 | useEventListener( 92 | () => window, 93 | events, 94 | () => { 95 | handleActivity(); 96 | }, 97 | { passive: true } 98 | ); 99 | 100 | $effect(() => { 101 | if (!detectVisibilityChanges || !document) return; 102 | useEventListener(document, ["visibilitychange"], () => { 103 | if (document.hidden) return; 104 | handleActivity(); 105 | }); 106 | }); 107 | } 108 | 109 | get lastActive(): number { 110 | return this.#lastActive; 111 | } 112 | 113 | get current(): boolean { 114 | return this.#current; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/is-idle/is-idle.test.svelte.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 2 | import { IsIdle } from "./is-idle.svelte.js"; 3 | import { testWithEffect, vitestSetTimeoutWrapper } from "$lib/test/util.svelte.js"; 4 | 5 | describe("IsIdle", () => { 6 | beforeEach(() => { 7 | vi.useFakeTimers(); 8 | }); 9 | afterEach(() => { 10 | vi.clearAllTimers(); 11 | }); 12 | 13 | const DEFAULT_IDLE_TIME = 500; 14 | describe("Default behaviors", () => { 15 | testWithEffect("Initially set to false", async () => { 16 | const idleState = new IsIdle(); 17 | expect(idleState.current).toBe(false); 18 | }); 19 | 20 | testWithEffect("IsIdle is set to true when no activity occurs", async () => { 21 | const idleState = new IsIdle(); 22 | 23 | vitestSetTimeoutWrapper(() => { 24 | expect(idleState.current).toBe(true); 25 | }, DEFAULT_IDLE_TIME); 26 | }); 27 | 28 | testWithEffect("IsIdle is set to false on click event", async () => { 29 | const idleState = new IsIdle(); 30 | 31 | vitestSetTimeoutWrapper(() => { 32 | expect(idleState.current).toBe(true); 33 | const input = document.createElement("input"); 34 | document.body.appendChild(input); 35 | input.click(); 36 | expect(idleState.current).toBe(false); 37 | }, DEFAULT_IDLE_TIME); 38 | }); 39 | }); 40 | 41 | describe("Args", () => { 42 | testWithEffect("IsIdle timer arg", async () => { 43 | const idleState = new IsIdle({ timeout: 300 }); 44 | vitestSetTimeoutWrapper(() => { 45 | expect(idleState.current).toBe(false); 46 | }, 200); 47 | vitestSetTimeoutWrapper(() => { 48 | expect(idleState.current).toBe(true); 49 | }, 300); 50 | }); 51 | 52 | testWithEffect("Initial state option gets overwritten when passed in", async () => { 53 | const idleState = new IsIdle({ initialState: true }); 54 | expect(idleState.current).toBe(true); 55 | }); 56 | 57 | it.skip("Events args work gets overwritten when passed in", async () => { 58 | const idleState = new IsIdle({ events: ["keypress"] }); 59 | 60 | vitestSetTimeoutWrapper(() => { 61 | const input = document.createElement("input"); 62 | document.body.appendChild(input); 63 | input.click(); 64 | expect(idleState.current).toBe(true); 65 | // TODO: add jest-dom to handle events 66 | }, DEFAULT_IDLE_TIME); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/is-in-viewport/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./is-in-viewport.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/is-in-viewport/is-in-viewport.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigurableWindow } from "$lib/internal/configurable-globals.js"; 2 | import type { MaybeElementGetter } from "$lib/internal/types.js"; 3 | import { 4 | useIntersectionObserver, 5 | type UseIntersectionObserverOptions, 6 | } from "../use-intersection-observer/use-intersection-observer.svelte.js"; 7 | 8 | export type IsInViewportOptions = ConfigurableWindow & UseIntersectionObserverOptions; 9 | 10 | /** 11 | * Tracks if an element is visible within the current viewport. 12 | * 13 | * @see {@link https://runed.dev/docs/utilities/is-in-viewport} 14 | */ 15 | export class IsInViewport { 16 | #isInViewport = $state(false); 17 | 18 | constructor(node: MaybeElementGetter, options?: IsInViewportOptions) { 19 | useIntersectionObserver( 20 | node, 21 | (intersectionObserverEntries) => { 22 | let isIntersecting = this.#isInViewport; 23 | let latestTime = 0; 24 | for (const entry of intersectionObserverEntries) { 25 | if (entry.time >= latestTime) { 26 | latestTime = entry.time; 27 | isIntersecting = entry.isIntersecting; 28 | } 29 | } 30 | this.#isInViewport = isIntersecting; 31 | }, 32 | options 33 | ); 34 | } 35 | 36 | get current() { 37 | return this.#isInViewport; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/is-mounted/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./is-mounted.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/is-mounted/is-mounted.svelte.ts: -------------------------------------------------------------------------------- 1 | import { untrack } from "svelte"; 2 | 3 | /** 4 | * Returns an object with the mounted state of the component 5 | * that invokes this function. 6 | * 7 | * @see {@link https://runed.dev/docs/utilities/is-mounted} 8 | */ 9 | export class IsMounted { 10 | #isMounted: boolean = $state(false); 11 | 12 | constructor() { 13 | $effect(() => { 14 | untrack(() => (this.#isMounted = true)); 15 | 16 | return () => { 17 | this.#isMounted = false; 18 | }; 19 | }); 20 | } 21 | 22 | get current(): boolean { 23 | return this.#isMounted; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/on-click-outside/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./on-click-outside.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/persisted-state/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./persisted-state.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/pressed-keys/index.ts: -------------------------------------------------------------------------------- 1 | export { PressedKeys } from "./pressed-keys.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/pressed-keys/pressed-keys.svelte.ts: -------------------------------------------------------------------------------- 1 | import { on } from "svelte/events"; 2 | import { createSubscriber } from "svelte/reactivity"; 3 | import { watch } from "$lib/utilities/watch/index.js"; 4 | import { defaultWindow, type ConfigurableWindow } from "$lib/internal/configurable-globals.js"; 5 | 6 | export type PressedKeysOptions = ConfigurableWindow; 7 | /** 8 | * Tracks which keys are currently pressed. 9 | * 10 | * @see {@link https://runed.dev/docs/utilities/pressed-keys} 11 | */ 12 | export class PressedKeys { 13 | #pressedKeys = $state([]); 14 | readonly #subscribe?: () => void; 15 | 16 | constructor(options: PressedKeysOptions = {}) { 17 | const { window = defaultWindow } = options; 18 | this.has = this.has.bind(this); 19 | 20 | if (!window) return; 21 | 22 | this.#subscribe = createSubscriber((update) => { 23 | const keydown = on(window, "keydown", (e) => { 24 | const key = e.key.toLowerCase(); 25 | if (!this.#pressedKeys.includes(key)) { 26 | this.#pressedKeys.push(key); 27 | } 28 | update(); 29 | }); 30 | 31 | const keyup = on(window, "keyup", (e) => { 32 | const key = e.key.toLowerCase(); 33 | 34 | // Special handling for modifier keys (meta, control, alt, shift) 35 | // This addresses issues with OS/browser intercepting certain key combinations 36 | // where non-modifier keyup events might not fire properly 37 | if (["meta", "control", "alt", "shift"].includes(key)) { 38 | // When a modifier key is released, clear all non-modifier keys 39 | // but keep other modifier keys that might still be pressed 40 | // This prevents keys from getting "stuck" in the pressed state 41 | this.#pressedKeys = this.#pressedKeys.filter((k) => 42 | ["meta", "control", "alt", "shift"].includes(k) 43 | ); 44 | } 45 | 46 | // Regular key removal 47 | this.#pressedKeys = this.#pressedKeys.filter((k) => k !== key); 48 | update(); 49 | }); 50 | 51 | // Handle window blur events (switching applications, clicking outside browser) 52 | // Reset all keys when user shifts focus away from the window 53 | const blur = on(window, "blur", () => { 54 | this.#pressedKeys = []; 55 | update(); 56 | }); 57 | 58 | // Handle tab visibility changes (switching browser tabs) 59 | // This catches cases where the window doesn't lose focus but the tab is hidden 60 | const visibilityChange = on(document, "visibilitychange", () => { 61 | if (document.visibilityState === "hidden") { 62 | this.#pressedKeys = []; 63 | update(); 64 | } 65 | }); 66 | 67 | return () => { 68 | keydown(); 69 | keyup(); 70 | blur(); 71 | visibilityChange(); 72 | }; 73 | }); 74 | } 75 | 76 | has(...keys: string[]): boolean { 77 | this.#subscribe?.(); 78 | const normalizedKeys = keys.map((key) => key.toLowerCase()); 79 | return normalizedKeys.every((key) => this.#pressedKeys.includes(key)); 80 | } 81 | 82 | get all(): string[] { 83 | this.#subscribe?.(); 84 | return this.#pressedKeys; 85 | } 86 | 87 | /** 88 | * Registers a callback to execute when specified key combination is pressed. 89 | * 90 | * @param keys - Array or single string of keys to monitor 91 | * @param callback - Function to execute when the key combination is matched 92 | */ 93 | onKeys(keys: string | string[], callback: () => void) { 94 | this.#subscribe?.(); 95 | 96 | const keysToMonitor = Array.isArray(keys) ? keys : [keys]; 97 | 98 | watch( 99 | () => this.all, 100 | () => { 101 | if (this.has(...keysToMonitor)) { 102 | callback(); 103 | } 104 | } 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/previous/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./previous.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/previous/previous.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { Getter } from "$lib/internal/types.js"; 2 | import { watch } from "../watch/watch.svelte.js"; 3 | 4 | /** 5 | * Holds the previous value of a getter. 6 | * 7 | * @see {@link https://runed.dev/docs/utilities/previous} 8 | */ 9 | export class Previous { 10 | #previous: T | undefined = $state(undefined); 11 | 12 | constructor(getter: Getter, initialValue?: T) { 13 | if (initialValue !== undefined) this.#previous = initialValue; 14 | 15 | watch( 16 | () => getter(), 17 | (_, v) => { 18 | this.#previous = v; 19 | } 20 | ); 21 | } 22 | 23 | get current(): T | undefined { 24 | return this.#previous; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/previous/previous.test.svelte.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "$lib/internal/utils/sleep.js"; 2 | import { testWithEffect } from "$lib/test/util.svelte.js"; 3 | import { describe } from "node:test"; 4 | import { expect } from "vitest"; 5 | import { Previous } from "./previous.svelte.js"; 6 | 7 | describe("usePrevious", () => { 8 | testWithEffect("Should return undefined initially", () => { 9 | const previous = new Previous(() => 0); 10 | expect(previous.current).toBe(undefined); 11 | }); 12 | 13 | testWithEffect("Should return initialValue initially, when passed", () => { 14 | const previous = new Previous(() => 1, 0); 15 | expect(previous.current).toBe(0); 16 | }); 17 | 18 | testWithEffect("Should return previous value", async () => { 19 | let count = $state(0); 20 | const previous = new Previous(() => count); 21 | 22 | await sleep(10); 23 | expect(previous.current).toBe(undefined); 24 | count = 1; 25 | await sleep(10); 26 | expect(previous.current).toBe(0); 27 | count = 2; 28 | await sleep(10); 29 | expect(previous.current).toBe(1); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/resource/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./resource.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/resource/msw-handlers.ts: -------------------------------------------------------------------------------- 1 | import { http, delay, HttpResponse } from "msw"; 2 | 3 | export type ResponseData = { 4 | id: number; 5 | name: string; 6 | email: string; 7 | }; 8 | 9 | export type SearchResponseData = { 10 | results: { id: number; title: string }[]; 11 | page: number; 12 | total: number; 13 | }; 14 | 15 | export const handlers = [ 16 | // Basic user endpoint 17 | http.get("https://api.example.com/users/:id", async ({ params }) => { 18 | await delay(50); 19 | return HttpResponse.json({ 20 | id: Number(params.id), 21 | name: `User ${params.id}`, 22 | email: `user${params.id}@example.com`, 23 | }); 24 | }), 25 | 26 | // Search endpoint with query params 27 | http.get("https://api.example.com/search", ({ request }) => { 28 | const url = new URL(request.url); 29 | const query = url.searchParams.get("q"); 30 | const page = Number(url.searchParams.get("page")) || 1; 31 | 32 | return HttpResponse.json({ 33 | results: [ 34 | { id: page * 1, title: `Result 1 for ${query}` }, 35 | { id: page * 2, title: `Result 2 for ${query}` }, 36 | ], 37 | page, 38 | total: 10, 39 | }); 40 | }), 41 | 42 | // Endpoint that can fail 43 | http.get("https://api.example.com/error-prone", () => { 44 | return new HttpResponse(null, { status: 500 }); 45 | }), 46 | ]; 47 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/scroll-state/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./scroll-state.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/state-history/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state-history.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/state-history/state-history.svelte.ts: -------------------------------------------------------------------------------- 1 | import { watch } from "../watch/watch.svelte.js"; 2 | import type { MaybeGetter, Setter } from "$lib/internal/types.js"; 3 | import { get } from "$lib/internal/utils/get.js"; 4 | 5 | type LogEvent = { 6 | snapshot: T; 7 | timestamp: number; 8 | }; 9 | 10 | type StateHistoryOptions = { 11 | capacity?: MaybeGetter; 12 | }; 13 | 14 | /** 15 | * Tracks the change history of a value, providing undo and redo capabilities. 16 | * 17 | * @see {@link https://runed.dev/docs/utilities/state-history} 18 | */ 19 | export class StateHistory { 20 | #redoStack: LogEvent[] = $state([]); 21 | #ignoreUpdate: boolean = false; 22 | #set: Setter; 23 | log: LogEvent[] = $state([]); 24 | readonly canUndo = $derived(this.log.length > 1); 25 | readonly canRedo = $derived(this.#redoStack.length > 0); 26 | 27 | constructor(value: MaybeGetter, set: Setter, options?: StateHistoryOptions) { 28 | this.#redoStack = []; 29 | this.#set = set; 30 | this.undo = this.undo.bind(this); 31 | this.redo = this.redo.bind(this); 32 | 33 | const addEvent = (event: LogEvent): void => { 34 | this.log.push(event); 35 | const capacity$ = get(options?.capacity); 36 | if (capacity$ && this.log.length > capacity$) { 37 | this.log = this.log.slice(-capacity$); 38 | } 39 | }; 40 | 41 | watch( 42 | () => get(value), 43 | (v) => { 44 | if (this.#ignoreUpdate) { 45 | this.#ignoreUpdate = false; 46 | return; 47 | } 48 | 49 | addEvent({ snapshot: v, timestamp: new Date().getTime() }); 50 | this.#redoStack = []; 51 | } 52 | ); 53 | 54 | watch( 55 | () => get(options?.capacity), 56 | (c) => { 57 | if (!c) return; 58 | this.log = this.log.slice(-c); 59 | } 60 | ); 61 | } 62 | 63 | undo(): void { 64 | const [prev, curr] = this.log.slice(-2); 65 | if (!curr || !prev) return; 66 | this.#ignoreUpdate = true; 67 | this.#redoStack.push(curr); 68 | this.log.pop(); 69 | this.#set(prev.snapshot); 70 | } 71 | 72 | redo(): void { 73 | const nextEvent = this.#redoStack.pop(); 74 | if (!nextEvent) return; 75 | this.#ignoreUpdate = true; 76 | this.log.push(nextEvent); 77 | this.#set(nextEvent.snapshot); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/state-history/state-history.test.svelte.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | 3 | describe("StateHistory", () => { 4 | test("dummy test", () => { 5 | expect(true).toBe(true); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/textarea-autosize/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./textarea-autosize.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/use-debounce/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./use-debounce.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/use-debounce/use-debounce.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeGetter } from "$lib/internal/types.js"; 2 | import { extract } from "../extract/extract.svelte.js"; 3 | 4 | type UseDebounceReturn = (( 5 | this: unknown, 6 | ...args: Args 7 | ) => Promise) & { 8 | cancel: () => void; 9 | runScheduledNow: () => Promise; 10 | pending: boolean; 11 | }; 12 | 13 | type DebounceContext = { 14 | timeout: ReturnType | null; 15 | runner: (() => Promise) | null; 16 | resolve: (value: Return) => void; 17 | reject: (reason: unknown) => void; 18 | promise: Promise; 19 | }; 20 | 21 | /** 22 | * Function that takes a callback, and returns a debounced version of it. 23 | * When calling the debounced function, it will wait for the specified time 24 | * before calling the original callback. If the debounced function is called 25 | * again before the time has passed, the timer will be reset. 26 | * 27 | * You can await the debounced function to get the value when it is eventually 28 | * called. 29 | * 30 | * The second parameter is the time to wait before calling the original callback. 31 | * Alternatively, it can also be a getter function that returns the time to wait. 32 | * 33 | * @see {@link https://runed.dev/docs/utilities/use-debounce} 34 | * 35 | * @param callback The callback to call when the time has passed. 36 | * @param wait The length of time to wait in ms, defaults to 250. 37 | */ 38 | export function useDebounce( 39 | callback: (...args: Args) => Return, 40 | wait?: MaybeGetter 41 | ): UseDebounceReturn { 42 | let context = $state | null>(null); 43 | const wait$ = $derived(extract(wait, 250)); 44 | 45 | function debounced(this: unknown, ...args: Args) { 46 | if (context) { 47 | // Old context will be reused so callers awaiting the promise will get the 48 | // new value 49 | if (context.timeout) { 50 | clearTimeout(context.timeout); 51 | } 52 | } else { 53 | // No old context, create a new one 54 | let resolve: (value: Return) => void; 55 | let reject: (reason: unknown) => void; 56 | const promise = new Promise((res, rej) => { 57 | resolve = res; 58 | reject = rej; 59 | }); 60 | 61 | context = { 62 | timeout: null, 63 | runner: null, 64 | promise, 65 | resolve: resolve!, 66 | reject: reject!, 67 | }; 68 | } 69 | 70 | context.runner = async () => { 71 | // Grab the context and reset it 72 | // -> new debounced calls will create a new context 73 | if (!context) return; 74 | const ctx = context; 75 | context = null; 76 | 77 | try { 78 | ctx.resolve(await callback.apply(this, args)); 79 | } catch (error) { 80 | ctx.reject(error); 81 | } 82 | }; 83 | 84 | context.timeout = setTimeout(context.runner, wait$); 85 | 86 | return context.promise; 87 | } 88 | 89 | debounced.cancel = async () => { 90 | if (!context || context.timeout === null) { 91 | // Wait one event loop to see if something triggered the debounced function 92 | await new Promise((resolve) => setTimeout(resolve, 0)); 93 | if (!context || context.timeout === null) return; 94 | } 95 | 96 | clearTimeout(context.timeout); 97 | context.reject("Cancelled"); 98 | context = null; 99 | }; 100 | 101 | debounced.runScheduledNow = async () => { 102 | if (!context || !context.timeout) { 103 | // Wait one event loop to see if something triggered the debounced function 104 | await new Promise((resolve) => setTimeout(resolve, 0)); 105 | if (!context || !context.timeout) return; 106 | } 107 | 108 | clearTimeout(context.timeout); 109 | context.timeout = null; 110 | 111 | await context.runner?.(); 112 | }; 113 | 114 | Object.defineProperty(debounced, "pending", { 115 | enumerable: true, 116 | get() { 117 | return !!context?.timeout; 118 | }, 119 | }); 120 | 121 | return debounced as unknown as UseDebounceReturn; 122 | } 123 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/use-event-listener/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./use-event-listener.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/use-geolocation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./use-geolocation.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/use-geolocation/use-geolocation.svelte.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultNavigator, 3 | type ConfigurableNavigator, 4 | } from "$lib/internal/configurable-globals.js"; 5 | import type { WritableProperties } from "$lib/internal/types.js"; 6 | 7 | export type UseGeolocationOptions = Partial & { 8 | /** 9 | * Whether to start the watcher immediately upon creation. If set to `false`, the watcher 10 | * will only start tracking the position when `resume()` is called. 11 | * 12 | * @defaultValue true 13 | */ 14 | immediate?: boolean; 15 | } & ConfigurableNavigator; 16 | 17 | type WritableGeolocationPosition = WritableProperties< 18 | Omit 19 | > & { 20 | coords: WritableProperties>; 21 | }; 22 | 23 | export type UseGeolocationPosition = Omit & { 24 | coords: Omit; 25 | }; 26 | 27 | export type UseGeolocationReturn = { 28 | readonly isSupported: boolean; 29 | readonly position: UseGeolocationPosition; 30 | readonly error: GeolocationPositionError | null; 31 | readonly isPaused: boolean; 32 | resume: () => void; 33 | pause: () => void; 34 | }; 35 | 36 | /** 37 | * Reactive access to the browser's [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API). 38 | * 39 | * @see https://runed.dev/docs/utilities/use-geolocation 40 | */ 41 | export function useGeolocation(options: UseGeolocationOptions = {}): UseGeolocationReturn { 42 | const { 43 | enableHighAccuracy = true, 44 | maximumAge = 30000, 45 | timeout = 27000, 46 | immediate = true, 47 | navigator = defaultNavigator, 48 | } = options; 49 | 50 | const isSupported = Boolean(navigator); 51 | 52 | let error = $state.raw(null); 53 | let position = $state({ 54 | timestamp: 0, 55 | coords: { 56 | accuracy: 0, 57 | latitude: Number.POSITIVE_INFINITY, 58 | longitude: Number.POSITIVE_INFINITY, 59 | altitude: null, 60 | altitudeAccuracy: null, 61 | heading: null, 62 | speed: null, 63 | }, 64 | }); 65 | let isPaused = $state(false); 66 | 67 | function updatePosition(_position: GeolocationPosition) { 68 | error = null; 69 | position.timestamp = _position.timestamp; 70 | position.coords.accuracy = _position.coords.accuracy; 71 | position.coords.altitude = _position.coords.altitude; 72 | position.coords.altitudeAccuracy = _position.coords.altitudeAccuracy; 73 | position.coords.heading = _position.coords.heading; 74 | position.coords.latitude = _position.coords.latitude; 75 | position.coords.longitude = _position.coords.longitude; 76 | position.coords.speed = _position.coords.speed; 77 | } 78 | 79 | let watcher: number; 80 | 81 | function resume() { 82 | if (!navigator) return; 83 | watcher = navigator.geolocation.watchPosition(updatePosition, (err) => (error = err), { 84 | enableHighAccuracy, 85 | maximumAge, 86 | timeout, 87 | }); 88 | isPaused = false; 89 | } 90 | 91 | function pause() { 92 | if (watcher && navigator) { 93 | navigator.geolocation.clearWatch(watcher); 94 | } 95 | isPaused = true; 96 | } 97 | 98 | $effect(() => { 99 | if (immediate) resume(); 100 | return () => pause(); 101 | }); 102 | 103 | return { 104 | get isSupported() { 105 | return isSupported; 106 | }, 107 | position, 108 | get error() { 109 | return error; 110 | }, 111 | get isPaused() { 112 | return isPaused; 113 | }, 114 | resume, 115 | pause, 116 | }; 117 | } 118 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/use-intersection-observer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./use-intersection-observer.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/use-intersection-observer/use-intersection-observer.svelte.ts: -------------------------------------------------------------------------------- 1 | import { extract } from "../extract/extract.svelte.js"; 2 | import type { MaybeElementGetter, MaybeGetter } from "$lib/internal/types.js"; 3 | import { get } from "$lib/internal/utils/get.js"; 4 | import { defaultWindow, type ConfigurableWindow } from "$lib/internal/configurable-globals.js"; 5 | 6 | export interface UseIntersectionObserverOptions 7 | extends Omit, 8 | ConfigurableWindow { 9 | /** 10 | * Whether to start the observer immediately upon creation. If set to `false`, the observer 11 | * will only start observing when `resume()` is called. 12 | * 13 | * @defaultValue true 14 | */ 15 | immediate?: boolean; 16 | 17 | /** 18 | * The root document/element to use as the bounding box for the intersection. 19 | */ 20 | root?: MaybeElementGetter; 21 | } 22 | 23 | /** 24 | * Watch for intersection changes of a target element. 25 | * 26 | * @see https://runed.dev/docs/utilities/useIntersectionObserver 27 | * @see https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver IntersectionObserver MDN 28 | */ 29 | export function useIntersectionObserver( 30 | target: MaybeGetter, 31 | callback: IntersectionObserverCallback, 32 | options: UseIntersectionObserverOptions = {} 33 | ) { 34 | const { 35 | root, 36 | rootMargin = "0px", 37 | threshold = 0.1, 38 | immediate = true, 39 | window = defaultWindow, 40 | } = options; 41 | 42 | let isActive = $state(immediate); 43 | let observer: IntersectionObserver | undefined; 44 | 45 | const targets = $derived.by(() => { 46 | const value = extract(target); 47 | return new Set(value ? (Array.isArray(value) ? value : [value]) : []); 48 | }); 49 | 50 | const stop = $effect.root(() => { 51 | $effect(() => { 52 | if (!targets.size || !isActive || !window) return; 53 | observer = new window.IntersectionObserver(callback, { 54 | rootMargin, 55 | root: get(root), 56 | threshold, 57 | }); 58 | for (const el of targets) observer.observe(el); 59 | 60 | return () => { 61 | observer?.disconnect(); 62 | }; 63 | }); 64 | }); 65 | 66 | $effect(() => { 67 | return stop; 68 | }); 69 | 70 | return { 71 | get isActive() { 72 | return isActive; 73 | }, 74 | stop, 75 | pause() { 76 | isActive = false; 77 | }, 78 | resume() { 79 | isActive = true; 80 | }, 81 | }; 82 | } 83 | 84 | export type UseIntersectionObserverReturn = ReturnType; 85 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/use-mutation-observer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./use-mutation-observer.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/use-mutation-observer/use-mutation-observer.svelte.ts: -------------------------------------------------------------------------------- 1 | import { extract } from "../extract/extract.svelte.js"; 2 | import type { MaybeGetter } from "$lib/internal/types.js"; 3 | import { defaultWindow, type ConfigurableWindow } from "$lib/internal/configurable-globals.js"; 4 | 5 | export interface UseMutationObserverOptions extends MutationObserverInit, ConfigurableWindow {} 6 | 7 | /** 8 | * Watch for changes being made to the DOM tree. 9 | * 10 | * @see https://runed.dev/docs/utilities/useMutationObserver 11 | * @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver MutationObserver MDN 12 | */ 13 | export function useMutationObserver( 14 | target: MaybeGetter, 15 | callback: MutationCallback, 16 | options: UseMutationObserverOptions = {} 17 | ) { 18 | const { window = defaultWindow } = options; 19 | let observer: MutationObserver | undefined; 20 | 21 | const targets = $derived.by(() => { 22 | const value = extract(target); 23 | return new Set(value ? (Array.isArray(value) ? value : [value]) : []); 24 | }); 25 | 26 | const stop = $effect.root(() => { 27 | $effect(() => { 28 | if (!targets.size || !window) return; 29 | observer = new window.MutationObserver(callback); 30 | for (const el of targets) observer.observe(el, options); 31 | 32 | return () => { 33 | observer?.disconnect(); 34 | observer = undefined; 35 | }; 36 | }); 37 | }); 38 | 39 | $effect(() => { 40 | return stop; 41 | }); 42 | 43 | return { 44 | stop, 45 | takeRecords() { 46 | return observer?.takeRecords(); 47 | }, 48 | }; 49 | } 50 | 51 | export type UseMutationObserverReturn = ReturnType; 52 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/use-resize-observer/index.ts: -------------------------------------------------------------------------------- 1 | export { useResizeObserver } from "./use-resize-observer.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/use-resize-observer/use-resize-observer.svelte.ts: -------------------------------------------------------------------------------- 1 | import { extract } from "../extract/extract.svelte.js"; 2 | import type { MaybeGetter } from "$lib/internal/types.js"; 3 | import { defaultWindow, type ConfigurableWindow } from "$lib/internal/configurable-globals.js"; 4 | 5 | export interface ResizeObserverSize { 6 | readonly inlineSize: number; 7 | readonly blockSize: number; 8 | } 9 | 10 | export interface ResizeObserverEntry { 11 | readonly target: Element; 12 | readonly contentRect: DOMRectReadOnly; 13 | readonly borderBoxSize?: ReadonlyArray; 14 | readonly contentBoxSize?: ReadonlyArray; 15 | readonly devicePixelContentBoxSize?: ReadonlyArray; 16 | } 17 | 18 | export type ResizeObserverCallback = ( 19 | entries: ReadonlyArray, 20 | observer: ResizeObserver 21 | ) => void; 22 | 23 | export interface UseResizeObserverOptions extends ConfigurableWindow { 24 | /** 25 | * Sets which box model the observer will observe changes to. Possible values 26 | * are `content-box` (the default), `border-box` and `device-pixel-content-box`. 27 | * 28 | * @default 'content-box' 29 | */ 30 | box?: ResizeObserverBoxOptions; 31 | } 32 | 33 | declare class ResizeObserver { 34 | constructor(callback: ResizeObserverCallback); 35 | disconnect(): void; 36 | observe(target: Element, options?: UseResizeObserverOptions): void; 37 | unobserve(target: Element): void; 38 | } 39 | 40 | /** 41 | * Reports changes to the dimensions of an Element's content or the border-box 42 | * 43 | * @see https://runed.dev/docs/utilities/useResizeObserver 44 | */ 45 | export function useResizeObserver( 46 | target: MaybeGetter, 47 | callback: ResizeObserverCallback, 48 | options: UseResizeObserverOptions = {} 49 | ) { 50 | const { window = defaultWindow } = options; 51 | let observer: ResizeObserver | undefined; 52 | 53 | const targets = $derived.by(() => { 54 | const value = extract(target); 55 | return new Set(value ? (Array.isArray(value) ? value : [value]) : []); 56 | }); 57 | 58 | const stop = $effect.root(() => { 59 | $effect(() => { 60 | if (!targets.size || !window) return; 61 | observer = new window.ResizeObserver(callback); 62 | for (const el of targets) observer.observe(el, options); 63 | 64 | return () => { 65 | observer?.disconnect(); 66 | observer = undefined; 67 | }; 68 | }); 69 | }); 70 | 71 | $effect(() => { 72 | return stop; 73 | }); 74 | 75 | return { 76 | stop, 77 | }; 78 | } 79 | 80 | export type UseResizeObserverReturn = ReturnType; 81 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/watch/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./watch.svelte.js"; 2 | -------------------------------------------------------------------------------- /packages/runed/src/lib/utilities/watch/watch.test.svelte.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect } from "vitest"; 2 | import { watch, watchOnce } from "./watch.svelte.js"; 3 | import { testWithEffect } from "$lib/test/util.svelte.js"; 4 | import { sleep } from "$lib/internal/utils/sleep.js"; 5 | 6 | describe("watch", () => { 7 | testWithEffect("watchers only track their dependencies", async () => { 8 | let count = $state(0); 9 | let runs = $state(0); 10 | 11 | watch( 12 | () => count, 13 | () => { 14 | runs = runs + 1; 15 | } 16 | ); 17 | 18 | // Watchers run immediately by default 19 | await sleep(0); 20 | expect(runs).toBe(1); 21 | 22 | count++; 23 | await sleep(0); 24 | expect(runs).toBe(2); 25 | }); 26 | 27 | testWithEffect("watchers initially pass `undefined` as the previous value", () => { 28 | return new Promise((resolve) => { 29 | const count = $state(0); 30 | 31 | watch( 32 | () => count, 33 | (count, prevCount) => { 34 | expect(count).toBe(0); 35 | expect(prevCount).toBe(undefined); 36 | resolve(); 37 | } 38 | ); 39 | }); 40 | }); 41 | 42 | testWithEffect( 43 | "watchers with an array of sources initially pass an empty array as the previous value", 44 | () => { 45 | return new Promise((resolve) => { 46 | const count = $state(1); 47 | const doubled = $derived(count * 2); 48 | 49 | watch([() => count, () => doubled], ([count, doubled], [prevCount, prevDoubled]) => { 50 | expect(count).toBe(1); 51 | expect(prevCount).toBe(undefined); 52 | expect(doubled).toBe(2); 53 | expect(prevDoubled).toBe(undefined); 54 | resolve(); 55 | }); 56 | }); 57 | } 58 | ); 59 | 60 | testWithEffect("lazy watchers pass the initial value as the previous value", () => { 61 | return new Promise((resolve) => { 62 | let count = $state(0); 63 | 64 | watch( 65 | () => count, 66 | (count, prevCount) => { 67 | expect(count).toBe(1); 68 | expect(prevCount).toBe(0); 69 | resolve(); 70 | }, 71 | { lazy: true } 72 | ); 73 | 74 | // Wait for the watcher's initial run to determine its dependencies. 75 | sleep(0).then(() => { 76 | count = 1; 77 | }); 78 | }); 79 | }); 80 | 81 | testWithEffect("once watchers only run once", async () => { 82 | let count = $state(0); 83 | let runs = 0; 84 | 85 | watchOnce( 86 | () => count, 87 | () => { 88 | runs++; 89 | } 90 | ); 91 | 92 | // Wait for the watcher's initial run to determine its dependencies. 93 | await sleep(0); 94 | 95 | count++; 96 | await sleep(0); 97 | expect(runs).toBe(1); 98 | 99 | count++; 100 | await sleep(0); 101 | expect(runs).toBe(1); 102 | }); 103 | 104 | testWithEffect("once watchers pass the initial value as the previous value", () => { 105 | return new Promise((resolve) => { 106 | let count = $state(0); 107 | 108 | watchOnce( 109 | () => count, 110 | (count, prevCount) => { 111 | expect(count).toBe(1); 112 | expect(prevCount).toBe(0); 113 | resolve(); 114 | } 115 | ); 116 | 117 | // Wait for the watcher's initial run to determine its dependencies. 118 | sleep(0).then(() => { 119 | count = 1; 120 | }); 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /packages/runed/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; 2 | 3 | /** @type {import('@sveltejs/kit').Config} */ 4 | const config = { 5 | preprocess: [vitePreprocess()], 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /packages/runed/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 | "noUncheckedIndexedAccess": true, 15 | "types": ["vitest/globals"], 16 | "experimentalDecorators": true 17 | }, 18 | "include": [ 19 | "./.svelte-kit/ambient.d.ts", 20 | "./.svelte-kit/non-ambient.d.ts", 21 | "./.svelte-kit/types/**/$types.d.ts", 22 | "./src/**/*.js", 23 | "./src/**/*.ts", 24 | "./src/**/*.svelte", 25 | "./vite.config.ts", 26 | "./svelte.config.js", 27 | "./setupTest.ts" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /packages/runed/vite.config.ts: -------------------------------------------------------------------------------- 1 | import process from "node:process"; 2 | import { sveltekit } from "@sveltejs/kit/vite"; 3 | import { defineConfig } from "vitest/config"; 4 | import { svelteTesting } from "@testing-library/svelte/vite"; 5 | import type { Plugin } from "vite"; 6 | 7 | const vitestBrowserConditionPlugin: Plugin = { 8 | name: "vite-plugin-vitest-browser-condition", 9 | configResolved({ resolve }: { resolve: { conditions: string[] } }) { 10 | if (process.env.VITEST) { 11 | resolve.conditions.unshift("browser"); 12 | } 13 | }, 14 | }; 15 | 16 | export default defineConfig({ 17 | plugins: [vitestBrowserConditionPlugin, sveltekit(), svelteTesting()], 18 | test: { 19 | includeSource: ["src/**/*.{js,ts,svelte}"], 20 | globals: true, 21 | coverage: { 22 | exclude: ["./setupTest.ts"], 23 | }, 24 | workspace: [ 25 | { 26 | extends: true, 27 | test: { 28 | setupFiles: ["./setupTest.ts"], 29 | include: ["src/**/*.{test,test.svelte,spec}.{js,ts}"], 30 | exclude: ["src/**/*.browser.{test,test.svelte,spec}.{js,ts}"], 31 | name: "unit", 32 | environment: "jsdom", 33 | }, 34 | }, 35 | { 36 | plugins: [sveltekit(), svelteTesting()], 37 | test: { 38 | include: ["src/**/*.browser.{test,test.svelte,spec}.{js,ts}"], 39 | name: "browser", 40 | browser: { 41 | instances: [ 42 | { 43 | browser: "chromium", 44 | }, 45 | ], 46 | enabled: true, 47 | provider: "playwright", 48 | headless: true, 49 | }, 50 | }, 51 | }, 52 | ], 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "sites/*" 4 | -------------------------------------------------------------------------------- /scripts/add-utility.mjs: -------------------------------------------------------------------------------- 1 | /* CLI tool to add a new utility. It asks for the utility name and then creates the respective files. */ 2 | import fs from "node:fs"; 3 | import readlineSync from "readline-sync"; 4 | 5 | function toKebabCase(str) { 6 | return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); 7 | } 8 | 9 | const utilsDir = "./packages/runed/src/lib/utilities"; 10 | const contentDir = "./sites/docs/src/content/utilities"; 11 | const demosDir = "./sites/docs/src/lib/components/demos"; 12 | 13 | const utilName = readlineSync.question("What is the name of the utility? "); 14 | const kebabUtil = toKebabCase(utilName); 15 | const utilDir = `${utilsDir}/${kebabUtil}`; 16 | const utilIndexFile = `${utilDir}/index.ts`; 17 | const utilMainFile = `${utilDir}/${kebabUtil}.svelte.ts`; 18 | const utilsBarrelFile = `${utilsDir}/index.ts`; 19 | const contentFile = `${contentDir}/${kebabUtil}.md`; 20 | const demoFile = `${demosDir}/${kebabUtil}.svelte`; 21 | 22 | fs.mkdirSync(utilDir, { recursive: true }); 23 | fs.writeFileSync(utilIndexFile, `export * from "./${kebabUtil}.svelte.js";`); 24 | fs.writeFileSync(utilMainFile, ""); 25 | fs.appendFileSync(utilsBarrelFile, `export * from "./${kebabUtil}/index.js";`); 26 | 27 | // Write the boilerplate code for the docs content file 28 | fs.writeFileSync( 29 | contentFile, 30 | `--- 31 | title: ${utilName} 32 | description: N/A 33 | category: New 34 | --- 35 | 36 | 39 | 40 | ## Demo 41 | 42 | 43 | 44 | ## Usage 45 | ` 46 | ); 47 | 48 | // Write the boilerplate code for the demo file 49 | fs.writeFileSync( 50 | demoFile, 51 | ` 52 | 56 | 57 | 58 | 59 | 60 | ` 61 | ); 62 | -------------------------------------------------------------------------------- /sites/docs/.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 | .contentlayer 12 | .contentlayer/ 13 | /.contentlayer 14 | .vercel -------------------------------------------------------------------------------- /sites/docs/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hunter Johnston 4 | Copyright (c) 2024 Thomas G. Lopes 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /sites/docs/README.md: -------------------------------------------------------------------------------- 1 | # Runed Documentation 2 | -------------------------------------------------------------------------------- /sites/docs/mdsx.config.js: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | import { defineConfig } from "mdsx"; 4 | import { baseRehypePlugins, baseRemarkPlugins } from "@svecodocs/kit/mdsxConfig"; 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 | -------------------------------------------------------------------------------- /sites/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "description": "Docs for Runed", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "pnpm \"/dev:/\"", 8 | "dev:content": "velite dev --watch", 9 | "dev:svelte": "pnpm build:search && vite dev", 10 | "build": "velite && pnpm build:search && vite build", 11 | "build:search": "node ./scripts/update-velite-output.js && 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 | "license": "MIT", 17 | "contributors": [ 18 | { 19 | "name": "Thomas G. Lopes", 20 | "url": "https://thomasglopes.com" 21 | }, 22 | { 23 | "name": "Hunter Johnston", 24 | "url": "https://github.com/huntabyte" 25 | } 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/svecosystem/runed.git" 30 | }, 31 | "devDependencies": { 32 | "@svecodocs/kit": "^0.2.1", 33 | "@sveltejs/adapter-auto": "^6.0.1", 34 | "@sveltejs/adapter-cloudflare": "^7.0.3", 35 | "@sveltejs/kit": "^2.20.7", 36 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 37 | "@tailwindcss/vite": "4.1.4", 38 | "mdsx": "^0.0.6", 39 | "phosphor-svelte": "^3.0.1", 40 | "runed": "workspace:^", 41 | "svelte": "^5.28.6", 42 | "svelte-check": "^4.1.6", 43 | "tailwindcss": "4.1.4", 44 | "typescript": "^5.7.2", 45 | "velite": "^0.2.1", 46 | "vite": "^6.3.5", 47 | "vitest": "^3.1.3" 48 | }, 49 | "type": "module" 50 | } 51 | -------------------------------------------------------------------------------- /sites/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 | -------------------------------------------------------------------------------- /sites/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 | -------------------------------------------------------------------------------- /sites/docs/src/app.css: -------------------------------------------------------------------------------- 1 | @import "@svecodocs/kit/theme-orange.css"; 2 | @import "@svecodocs/kit/globals.css"; 3 | @source "../node_modules/@svecodocs/kit"; 4 | -------------------------------------------------------------------------------- /sites/docs/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 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 | -------------------------------------------------------------------------------- /sites/docs/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /sites/docs/src/content/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | description: Learn how to install and use Runed in your projects. 4 | category: Anchor 5 | --- 6 | 7 | ## Installation 8 | 9 | Install Runed using your favorite package manager: 10 | 11 | ```bash 12 | npm install runed 13 | ``` 14 | 15 | ## Usage 16 | 17 | Import one of the utilities you need to either a `.svelte` or `.svelte.js|ts` file and start using 18 | it: 19 | 20 | ```svelte title="component.svelte" 21 | 26 | 27 | 28 | 29 | {#if activeElement.current === inputElement} 30 | The input element is active! 31 | {/if} 32 | ``` 33 | 34 | or 35 | 36 | ```ts title="some-module.svelte.ts" 37 | import { activeElement } from "runed"; 38 | 39 | function logActiveElement() { 40 | $effect(() => { 41 | console.log("Active element is ", activeElement.current); 42 | }); 43 | } 44 | 45 | logActiveElement(); 46 | ``` 47 | -------------------------------------------------------------------------------- /sites/docs/src/content/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: Runes are magic, but what good is magic if you don't have a wand? 4 | category: Anchor 5 | --- 6 | 7 | Runed is a collection of utilities for Svelte 5 that make composing powerful applications and 8 | libraries a breeze, leveraging the power of [Svelte Runes](https://svelte.dev/blog/runes). 9 | 10 | ## Why Runed? 11 | 12 | Svelte 5 Runes unlock immense power by providing a set of primitives that allow us to build 13 | impressive applications and libraries with ease. However, building complex applications often 14 | requires more than just the primitives provided by Svelte Runes. 15 | 16 | Runed takes those primitives to the next level by providing: 17 | 18 | - **Powerful Utilities**: A set of carefully crafted utility functions and classes that simplify 19 | common tasks and reduce boilerplate. 20 | - **Collective Efforts**: We often find ourselves writing the same utility functions over and over 21 | again. Runed aims to provide a single source of truth for these utilities, allowing the community 22 | to contribute, test, and benefit from them. 23 | - **Consistency**: A consistent set of APIs and behaviors across all utilities, so you can focus on 24 | building your projects instead of constantly learning new APIs. 25 | - **Reactivity First**: Powered by Svelte 5's new reactivity system, Runed utilities are designed to 26 | handle reactive state and side effects with ease. 27 | - **Type Safety**: Full TypeScript support to catch errors early and provide a better developer 28 | experience. 29 | 30 | ## Ideas and Principles 31 | 32 | #### Embrace the Magic of Runes 33 | 34 | Svelte Runes are a powerful new paradigm. Runed fully embraces this concept and explores its 35 | potential. Our goal is to make working with Runes feel as natural and intuitive as possible. 36 | 37 | #### Enhance, Don't Replace 38 | 39 | Runed is not here to replace Svelte's core functionality, but to enhance and extend it. Our 40 | utilities should feel like a natural extension of Svelte, not a separate framework. 41 | 42 | #### Progressive Complexity 43 | 44 | Simple things should be simple, complex things should be possible. Runed provides easy-to-use 45 | defaults while allowing for advanced customization when needed. 46 | 47 | #### Open Source and Community Collaboration 48 | 49 | Runed is an open-source, MIT licensed project that welcomes all forms of contributions from the 50 | community. Whether it's bug reports, feature requests, or code contributions, your input will help 51 | make Runed the best it can be. 52 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/active-element.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: activeElement 3 | description: Track and access the currently focused DOM element 4 | category: Elements 5 | --- 6 | 7 | 10 | 11 | `activeElement` provides reactive access to the currently focused DOM element in your application, 12 | similar to `document.activeElement` but with reactive updates. 13 | 14 | - Updates synchronously with DOM focus changes 15 | - Returns `null` when no element is focused 16 | - Safe to use with SSR (Server-Side Rendering) 17 | - Lightweight alternative to manual focus tracking 18 | - Searches through Shadow DOM boundaries for the true active element 19 | 20 | ## Demo 21 | 22 | 23 | 24 | ## Usage 25 | 26 | ```svelte 27 | 30 | 31 |

32 | Currently active element: 33 | {activeElement.current?.localName ?? "No active element found"} 34 |

35 | ``` 36 | 37 | ## Custom Document 38 | 39 | If you wish to scope the focus tracking within a custom document or shadow root, you can pass a 40 | `DocumentOrShadowRoot` to the `ActiveElement` options: 41 | 42 | ```svelte 43 | 50 | ``` 51 | 52 | ## Type Definition 53 | 54 | ```ts 55 | interface ActiveElement { 56 | readonly current: Element | null; 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/animation-frames.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: AnimationFrames 3 | description: A wrapper for requestAnimationFrame with FPS control and frame metrics 4 | category: Animation 5 | --- 6 | 7 | 10 | 11 | `AnimationFrames` provides a declarative API over the browser's 12 | [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame), 13 | offering FPS limiting capabilities and frame metrics while handling cleanup automatically. 14 | 15 | ## Demo 16 | 17 | 18 | 19 | ## Usage 20 | 21 | ```svelte 22 | 41 | 42 |
{stats}
43 | 46 |

47 | FPS limit: {fpsLimit}{fpsLimit === 0 ? " (not limited)" : ""} 48 |

49 | (fpsLimit = value[0] ?? 0)} 52 | min={0} 53 | max={144} /> 54 | ``` 55 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/context.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Context 3 | description: 4 | A wrapper around Svelte's Context API that provides type safety and improved ergonomics for 5 | sharing data between components. 6 | category: State 7 | --- 8 | 9 | 12 | 13 | Context allows you to pass data through the component tree without explicitly passing props through 14 | every level. It's useful for sharing data that many components need, like themes, authentication 15 | state, or localization preferences. 16 | 17 | The `Context` class provides a type-safe way to define, set, and retrieve context values. 18 | 19 | ## Usage 20 | 21 | 22 | 23 | Creating a Context 24 | 25 | First, create a `Context` instance with the type of value it will hold: 26 | 27 | ```ts title="context.ts" 28 | import { Context } from "runed"; 29 | 30 | export const myTheme = new Context<"light" | "dark">("theme"); 31 | ``` 32 | 33 | Creating a `Context` instance only defines the context - it doesn't actually set any value. The 34 | value passed to the constructor (`"theme"` in this example) is just an identifier used for debugging 35 | and error messages. 36 | 37 | Think of this step as creating a "container" that will later hold your context value. The container 38 | is typed (in this case to only accept `"light"` or `"dark"` as values) but remains empty until you 39 | explicitly call `myTheme.set()` during component initialization. 40 | 41 | This separation between defining and setting context allows you to: 42 | 43 | - Keep context definitions in separate files 44 | - Reuse the same context definition across different parts of your app 45 | - Maintain type safety throughout your application 46 | - Set different values for the same context in different component trees 47 | 48 | Setting Context Values 49 | 50 | Set the context value in a parent component during initialization. 51 | 52 | ```svelte title="+layout.svelte" 53 | 59 | 60 | {@render children?.()} 61 | ``` 62 | 63 | 64 | 65 | Context must be set during component initialization, similar to lifecycle functions like `onMount`. 66 | You cannot set context inside event handlers or callbacks. 67 | 68 | 69 | 70 | Reading Context Values 71 | 72 | Child components can access the context using `get()` or `getOr()` 73 | 74 | ```svelte title="+page.svelte" 75 | 82 | ``` 83 | 84 | 85 | 86 | ## Type Definition 87 | 88 | ```ts 89 | class Context { 90 | /** 91 | * @param name The name of the context. 92 | * This is used for generating the context key and error messages. 93 | */ 94 | constructor(name: string) {} 95 | 96 | /** 97 | * The key used to get and set the context. 98 | * 99 | * It is not recommended to use this value directly. 100 | * Instead, use the methods provided by this class. 101 | */ 102 | get key(): symbol; 103 | 104 | /** 105 | * Checks whether this has been set in the context of a parent component. 106 | * 107 | * Must be called during component initialization. 108 | */ 109 | exists(): boolean; 110 | 111 | /** 112 | * Retrieves the context that belongs to the closest parent component. 113 | * 114 | * Must be called during component initialization. 115 | * 116 | * @throws An error if the context does not exist. 117 | */ 118 | get(): TContext; 119 | 120 | /** 121 | * Retrieves the context that belongs to the closest parent component, 122 | * or the given fallback value if the context does not exist. 123 | * 124 | * Must be called during component initialization. 125 | */ 126 | getOr(fallback: TFallback): TContext | TFallback; 127 | 128 | /** 129 | * Associates the given value with the current component and returns it. 130 | * 131 | * Must be called during component initialization. 132 | */ 133 | set(context: TContext): TContext; 134 | } 135 | ``` 136 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/debounced.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Debounced 3 | description: A wrapper over `useDebounce` that returns a debounced state. 4 | category: State 5 | --- 6 | 7 | 10 | 11 | ## Demo 12 | 13 | 14 | 15 | ## Usage 16 | 17 | This is a simple wrapper over [`useDebounce`](https://runed.dev/docs/utilities/use-debounce) that 18 | returns a debounced state. 19 | 20 | ```svelte 21 | 27 | 28 |
29 | 30 |

You searched for: {debounced.current}

31 |
32 | ``` 33 | 34 | You may cancel the pending update, run it immediately, or set a new value. Setting a new value 35 | immediately also cancels any pending updates. 36 | 37 | ```ts 38 | let count = $state(0); 39 | const debounced = new Debounced(() => count, 500); 40 | count = 1; 41 | debounced.cancel(); 42 | // after a while... 43 | console.log(debounced.current); // Still 0! 44 | 45 | count = 2; 46 | console.log(debounced.current); // Still 0! 47 | debounced.setImmediately(count); 48 | console.log(debounced.current); // 2 49 | 50 | count = 3; 51 | console.log(debounced.current); // 2 52 | await debounced.updateImmediately(); 53 | console.log(debounced.current); // 3 54 | ``` 55 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/element-rect.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ElementRect 3 | description: Track element dimensions and position reactively 4 | category: Elements 5 | --- 6 | 7 | 10 | 11 | `ElementRect` provides reactive access to an element's dimensions and position information, 12 | automatically updating when the element's size or position changes. 13 | 14 | ## Demo 15 | 16 | 17 | 18 | ## Usage 19 | 20 | ```svelte 21 | 27 | 28 | 29 | 30 |

Width: {rect.width} Height: {rect.height}

31 | 32 |
{JSON.stringify(rect.current, null, 2)}
33 | ``` 34 | 35 | ## Type Definition 36 | 37 | ```ts 38 | type Rect = Omit; 39 | 40 | interface ElementRectOptions { 41 | initialRect?: DOMRect; 42 | } 43 | 44 | class ElementRect { 45 | constructor(node: MaybeGetter, options?: ElementRectOptions); 46 | readonly current: Rect; 47 | readonly width: number; 48 | readonly height: number; 49 | readonly top: number; 50 | readonly left: number; 51 | readonly right: number; 52 | readonly bottom: number; 53 | readonly x: number; 54 | readonly y: number; 55 | } 56 | ``` 57 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/element-size.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ElementSize 3 | description: Track element dimensions reactively 4 | category: Elements 5 | --- 6 | 7 | 10 | 11 | `ElementSize` provides reactive access to an element's width and height, automatically updating when 12 | the element's dimensions change. Similar to `ElementRect` but focused only on size measurements. 13 | 14 | ## Demo 15 | 16 | 17 | 18 | ## Usage 19 | 20 | ```svelte 21 | 27 | 28 | 29 | 30 |

Width: {size.width} Height: {size.height}

31 | ``` 32 | 33 | ## Type Definition 34 | 35 | ```ts 36 | interface ElementSize { 37 | readonly width: number; 38 | readonly height: number; 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/extract.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: extract 3 | description: Resolve the value of a getter or static variable 4 | category: Reactivity 5 | --- 6 | 7 | In libraries like Runed, it's common to pass state reactively using getters (functions that return a 8 | value), a common pattern to pass reactivity across boundaries. 9 | 10 | ```ts 11 | // For example... 12 | import { Previous } from "runed"; 13 | 14 | let count = $state(0); 15 | const previous = new Previous(() => count); 16 | ``` 17 | 18 | However, some APIs accept either a reactive getter or a static value (including `undefined`): 19 | 20 | ```ts 21 | let search = $state(""); 22 | let debounceTime = $state(500); 23 | 24 | // with a reactive value 25 | const d1 = new Debounced( 26 | () => search, 27 | () => debounceTime 28 | ); 29 | 30 | // with a static value 31 | const d2 = new Debounced(() => search, 500); 32 | 33 | // no defined value 34 | const d3 = new Debounced(() => search); 35 | ``` 36 | 37 | When writing utility functions, dealing with both types can lead to verbose and repetitive logic: 38 | 39 | ```ts 40 | setTimeout( 41 | /* ... */, 42 | typeof wait === "function" ? (wait() ?? 250) : (wait ?? 250) 43 | ); 44 | ``` 45 | 46 | This is where `extract` comes in. 47 | 48 | ## Usage 49 | 50 | The `extract` utility resolves either a getter or static value to a plain value. This helps you 51 | write cleaner, safer utilities. 52 | 53 | ```ts 54 | import { extract } from "runed"; 55 | 56 | /** 57 | * Triggers confetti at a given interval. 58 | * @param intervalProp Time between confetti bursts, in ms. Defaults to 100. 59 | */ 60 | function throwConfetti(intervalProp?: MaybeGetter) { 61 | const interval = $derived(extract(intervalProp, 100)); 62 | // ... 63 | } 64 | ``` 65 | 66 | ## Behavior 67 | 68 | Given a `MaybeGetter`, `extract(input, fallback)` resolves as follows: 69 | 70 | | Case | Result | 71 | | ------------------------------------------- | --------------------------- | 72 | | `input` is a value | Returns the value | 73 | | `input` is `undefined` | Returns the fallback | 74 | | `input` is a function returning a value | Returns the function result | 75 | | `input` is a function returning `undefined` | Returns the fallback | 76 | 77 | The fallback is _optional_. If you omit it, `extract()` returns `T | undefined`. 78 | 79 | ## Types 80 | 81 | ```ts 82 | function extract(input: MaybeGetter, fallback: T): T; 83 | function extract(input: MaybeGetter): T | undefined; 84 | ``` 85 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/is-focus-within.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: IsFocusWithin 3 | description: 4 | A utility that tracks whether any descendant element has focus within a specified container 5 | element. 6 | category: Elements 7 | --- 8 | 9 | 12 | 13 | `IsFocusWithin` reactively tracks focus state within a container element, updating automatically 14 | when focus changes. 15 | 16 | ## Demo 17 | 18 | 19 | 20 | ## Usage 21 | 22 | ```svelte 23 | 29 | 30 |

Focus within form: {focusWithinForm.current}

31 |
32 | 33 | 34 |
35 | ``` 36 | 37 | ## Type Definition 38 | 39 | ```ts 40 | class IsFocusWithin { 41 | constructor(node: MaybeGetter); 42 | readonly current: boolean; 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/is-idle.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: IsIdle 3 | description: Track if a user is idle and the last time they were active. 4 | category: Sensors 5 | --- 6 | 7 | 10 | 11 | `IsIdle` tracks user activity and determines if they're idle based on a configurable timeout. It 12 | monitors mouse movement, keyboard input, and touch events to detect user interaction. 13 | 14 | ## Demo 15 | 16 | 17 | 18 | ## Usage 19 | 20 | ```svelte 21 | 26 | 27 |

Idle: {idle.current}

28 |

29 | Last active: {new Date(idle.lastActive).toLocaleTimeString()} 30 |

31 | ``` 32 | 33 | ## Type Definitions 34 | 35 | ```ts 36 | interface IsIdleOptions { 37 | /** 38 | * The events that should set the idle state to `true` 39 | * 40 | * @default ['mousemove', 'mousedown', 'resize', 'keydown', 'touchstart', 'wheel'] 41 | */ 42 | events?: MaybeGetter<(keyof WindowEventMap)[]>; 43 | /** 44 | * The timeout in milliseconds before the idle state is set to `true`. Defaults to 60 seconds. 45 | * 46 | * @default 60000 47 | */ 48 | timeout?: MaybeGetter; 49 | /** 50 | * Detect document visibility changes 51 | * 52 | * @default false 53 | */ 54 | detectVisibilityChanges?: MaybeGetter; 55 | /** 56 | * The initial state of the idle property 57 | * 58 | * @default false 59 | */ 60 | initialState?: boolean; 61 | } 62 | 63 | class IsIdle { 64 | constructor(options?: IsIdleOptions); 65 | readonly current: boolean; 66 | readonly lastActive: number; 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/is-in-viewport.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: IsInViewport 3 | description: Track if an element is visible within the current viewport. 4 | category: Elements 5 | --- 6 | 7 | 10 | 11 | `IsInViewport` uses the [`useIntersectionObserver`](/docs/utilities/use-intersection-observer) 12 | utility to track if an element is visible within the current viewport. 13 | 14 | It accepts an element or getter that returns an element and an optional `options` object that aligns 15 | with the [`useIntersectionObserver`](/docs/utilities/use-intersection-observer) utility options. 16 | 17 | ## Demo 18 | 19 | 20 | 21 | ## Usage 22 | 23 | ```svelte 24 | 30 | 31 |

Target node

32 | 33 |

Target node in viewport: {inViewport.current}

34 | ``` 35 | 36 | ## Type Definition 37 | 38 | ```ts 39 | import { type UseIntersectionObserverOptions } from "runed"; 40 | export type IsInViewportOptions = UseIntersectionObserverOptions; 41 | 42 | export declare class IsInViewport { 43 | constructor(node: MaybeGetter, options?: IsInViewportOptions); 44 | get current(): boolean; 45 | } 46 | ``` 47 | 48 | 49 |
50 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/is-mounted.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: IsMounted 3 | description: A class that returns the mounted state of the component it's called in. 4 | category: Component 5 | --- 6 | 7 | 10 | 11 | ## Demo 12 | 13 | 14 | 15 | ## Usage 16 | 17 | ```svelte 18 | 23 | ``` 24 | 25 | Which is a shorthand for one of the following: 26 | 27 | ```svelte 28 | 37 | ``` 38 | 39 | or 40 | 41 | ```svelte 42 | 51 | ``` 52 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/persisted-state.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: PersistedState 3 | description: 4 | A reactive state manager that persists and synchronizes state across browser sessions and tabs 5 | using Web Storage APIs. 6 | category: State 7 | --- 8 | 9 | 13 | 14 | `PersistedState` provides a reactive state container that automatically persists data to browser 15 | storage and optionally synchronizes changes across browser tabs in real-time. 16 | 17 | ## Demo 18 | 19 | 20 | 21 | You can refresh this page and/or open it in another tab to see the count state being persisted 22 | and synchronized across sessions and tabs. 23 | 24 | 25 | ## Usage 26 | 27 | Initialize `PersistedState` by providing a unique key and an initial value for the state. 28 | 29 | ```svelte 30 | 35 | 36 |
37 | 38 | 39 | 40 |

Count: {count.current}

41 |
42 | ``` 43 | 44 | ## Configuration Options 45 | 46 | `PersistedState` includes an `options` object that allows you to customize the behavior of the state 47 | manager. 48 | 49 | ```ts 50 | const state = new PersistedState("user-preferences", initialValue, { 51 | // Use sessionStorage instead of localStorage (default: 'local') 52 | storage: "session", 53 | 54 | // Disable cross-tab synchronization (default: true) 55 | syncTabs: false, 56 | 57 | // Custom serialization handlers 58 | serializer: { 59 | serialize: superjson.stringify, 60 | deserialize: superjson.parse 61 | } 62 | }); 63 | ``` 64 | 65 | ### Storage Options 66 | 67 | - `'local'`: Data persists until explicitly cleared 68 | - `'session'`: Data persists until the browser session ends 69 | 70 | ### Cross-Tab Synchronization 71 | 72 | When `syncTabs` is enabled (default), changes are automatically synchronized across all browser tabs 73 | using the storage event. 74 | 75 | ### Custom Serialization 76 | 77 | Provide custom `serialize` and `deserialize` functions to handle complex data types: 78 | 79 | ```ts 80 | import superjson from "superjson"; 81 | 82 | // Example with Date objects 83 | const lastAccessed = new PersistedState("last-accessed", new Date(), { 84 | serializer: { 85 | serialize: superjson.stringify, 86 | deserialize: superjson.parse 87 | } 88 | }); 89 | ``` 90 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/pressed-keys.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: PressedKeys 3 | description: Tracks which keys are currently pressed 4 | category: Sensors 5 | --- 6 | 7 | 10 | 11 | ## Demo 12 | 13 | 14 | 15 | ## Usage 16 | 17 | With an instance of `PressedKeys`, you can use the `has` method. 18 | 19 | ```ts 20 | const keys = new PressedKeys(); 21 | 22 | const isArrowDownPressed = $derived(keys.has("ArrowDown")); 23 | const isCtrlAPressed = $derived(keys.has("Control", "a")); 24 | ``` 25 | 26 | Or get all of the currently pressed keys: 27 | 28 | ```ts 29 | const keys = new PressedKeys(); 30 | console.log(keys.all); 31 | ``` 32 | 33 | Or register a callback to execute when specified key combination is pressed: 34 | 35 | ```ts 36 | const keys = new PressedKeys(); 37 | keys.onKeys(["meta", "k"], () => { 38 | console.log("open command palette"); 39 | }); 40 | ``` 41 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/previous.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Previous 3 | description: A utility that tracks and provides access to the previous value of a reactive getter. 4 | category: State 5 | --- 6 | 7 | 10 | 11 | The `Previous` utility creates a reactive wrapper that maintains the previous value of a getter 12 | function. This is particularly useful when you need to compare state changes or implement transition 13 | effects. 14 | 15 | ## Demo 16 | 17 | 18 | 19 | ## Usage 20 | 21 | ```svelte 22 | 28 | 29 |
30 | 31 |
Previous: {`${previous.current}`}
32 |
33 | ``` 34 | 35 | ## Type Definition 36 | 37 | ```ts 38 | class Previous { 39 | constructor(getter: () => T); 40 | 41 | readonly current: T; // Previous value 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/scroll-state.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ScrollState 3 | description: 4 | Track scroll position, direction, and edge states with support for programmatic scrolling. 5 | category: Elements 6 | --- 7 | 8 | 11 | 12 | ## Demo 13 | 14 | 15 | 16 | ## Overview 17 | 18 | `ScrollState` is a reactive utility that lets you: 19 | 20 | - Track scroll positions (`x` / `y`) 21 | - Detect scroll direction (`left`, `right`, `top`, `bottom`) 22 | - Determine if the user has scrolled to an edge (`arrived` state) 23 | - Perform programmatic scrolling (`scrollTo`, `scrollToTop`, `scrollToBottom`) 24 | - Listen to scroll and scroll-end events 25 | - Respect flex, RTL, and reverse layout modes 26 | 27 | Inspired by [VueUse's `useScroll`](https://vueuse.org/useScroll), this utility is built for Svelte 28 | and works with DOM elements, the `window`, or `document`. 29 | 30 | ## Usage 31 | 32 | ```svelte 33 | 42 | 43 |
44 | 45 |
46 | ``` 47 | 48 | You can now access: 49 | 50 | - `scroll.x` and `scroll.y` — current scroll positions (reactive, get/set) 51 | 52 | - `scroll.directions` — active scroll directions 53 | 54 | - `scroll.arrived` — whether the scroll has reached each edge 55 | 56 | - `scroll.scrollTo(x, y)` — programmatic scroll 57 | 58 | - `scroll.scrollToTop()` and `scroll.scrollToBottom()` — helpers 59 | 60 | ## Options 61 | 62 | You can configure `ScrollState` via the following options: 63 | 64 | | Option | Type | Description | 65 | | ---------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------- | 66 | | `element` | `MaybeGetter` | The scroll container (required). | 67 | | `idle` | `MaybeGetter` | Debounce time (ms) after scroll ends. Default: `200`. | 68 | | `offset` | `{ top?, bottom?, left?, right? }` | Pixel thresholds for "arrived" state detection. Default: `0` for all. | 69 | | `onScroll` | `(e: Event) => void` | Callback for scroll events. | 70 | | `onStop` | `(e: Event) => void` | Callback after scrolling stops. | 71 | | `eventListenerOptions` | `AddEventListenerOptions` | Scroll listener options. Default: `{ passive: true, capture: false }`. | 72 | | `behavior` | `ScrollBehavior` | Scroll behavior: `"auto"`, `"smooth"`, etc. Default: `"auto"`. | 73 | | `onError` | `(error: unknown) => void` | Optional error handler. Default: `console.error`. | 74 | 75 | ## Notes 76 | 77 | - Both scroll position (`x`, `y`) and edge arrival state (`arrived`) are reactive values. 78 | 79 | - You can programmatically change `scroll.x` and `scroll.y`, and the element will scroll 80 | accordingly. 81 | 82 | - Layout direction and reverse flex settings are respected when calculating edge states. 83 | 84 | - Debounced `onStop` is invoked after scrolling ends and the user is idle. 85 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/state-history.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: StateHistory 3 | description: Track state changes with undo/redo capabilities 4 | category: State 5 | --- 6 | 7 | 10 | 11 | ## Demo 12 | 13 | 14 | 15 | ## Usage 16 | 17 | `StateHistory` tracks a getter's return value, logging each change into an array. A setter is also 18 | required to use the `undo` and `redo` functions. 19 | 20 | 21 | ```ts 22 | import { StateHistory } from "runed"; 23 | 24 | let count = $state(0); 25 | const history = new StateHistory(() => count, (c) => (count = c)); 26 | history.log[0]; // { snapshot: 0, timestamp: ... } 27 | ``` 28 | 29 | Besides `log`, the returned object contains `undo` and `redo` functionality. 30 | 31 | 32 | ```svelte 33 | 39 | 40 |

{count}

41 | 42 | 43 | 44 | 45 | 46 | 47 | ``` 48 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/textarea-autosize.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: TextareaAutosize 3 | description: Automatically adjust a textarea's height based on its content. 4 | category: Elements 5 | --- 6 | 7 | 10 | 11 | ## Demo 12 | 13 | 14 | 15 | ## Overview 16 | 17 | `TextareaAutosize` is a utility that makes ` 41 | ``` 42 | 43 | As you type, the textarea will automatically resize vertically to fit the content. 44 | 45 | ## Options 46 | 47 | You can customize behavior via the following options: 48 | 49 | | Option | Type | Description | 50 | | ----------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------- | 51 | | `element` | `Getter` | The target textarea (required). | 52 | | `input` | `Getter` | Reactive input value (required). | 53 | | `onResize` | `() => void` | Called whenever the height is updated. | 54 | | `styleProp` | `"height"` \| `"minHeight"` | CSS property to control size. `"height"` resizes both ways. `"minHeight"` grows only. Default: `"height"`. | 55 | | `maxHeight` | `number` | Maximum height in pixels before scroll appears. Default: unlimited. | 56 | 57 | ## Behavior 58 | 59 | Internally, `TextareaAutosize`: 60 | 61 | - Creates an invisible, off-screen ` 40 | ``` 41 | 42 | You can stop the resize observer at any point by calling the `stop` method. 43 | 44 | ```ts 45 | const { stop } = useResizeObserver(/* ... */); 46 | stop(); 47 | ``` 48 | -------------------------------------------------------------------------------- /sites/docs/src/content/utilities/watch.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: watch 3 | description: Watch for changes and run a callback 4 | category: Reactivity 5 | --- 6 | 7 | Runes provide a handy way of running a callback when reactive values change: 8 | [`$effect`](https://svelte-5-preview.vercel.app/docs/runes#$effect). It automatically detects when 9 | inner values change, and re-runs the callback. 10 | 11 | `$effect` is great, but sometimes you want to manually specify which values should trigger the 12 | callback. Svelte provides an `untrack` function, allowing you to specify that a dependency 13 | _shouldn't_ be tracked, but it doesn't provide a way to say that _only certain values_ should be 14 | tracked. 15 | 16 | `watch` does exactly that. It accepts a getter function, which returns the dependencies of the 17 | effect callback. 18 | 19 | ## Usage 20 | 21 | ### watch 22 | 23 | Runs a callback whenever one of the sources change. 24 | 25 | 26 | ```ts 27 | import { watch } from "runed"; 28 | 29 | let count = $state(0); 30 | watch(() => count, () => { 31 | console.log(count); 32 | }); 33 | ``` 34 | 35 | The callback receives two arguments: The current value of the sources, and the previous value. 36 | 37 | 38 | ```ts 39 | let count = $state(0); 40 | watch(() => count, (curr, prev) => { 41 | console.log(`count is ${curr}, was ${prev}`); 42 | }); 43 | ``` 44 | 45 | You can also send in an array of sources: 46 | 47 | 48 | ```ts 49 | let age = $state(20); 50 | let name = $state("bob"); 51 | watch([() => age, () => name], ([age, name], [prevAge, prevName]) => { 52 | // ... 53 | }); 54 | ``` 55 | 56 | `watch` also accepts an `options` object. 57 | 58 | ```ts 59 | watch(sources, callback, { 60 | // First run will only happen after sources change when set to true. 61 | // By default, its false. 62 | lazy: true 63 | }); 64 | ``` 65 | 66 | ### watch.pre 67 | 68 | `watch.pre` is similar to `watch`, but it uses 69 | [`$effect.pre`](https://svelte-5-preview.vercel.app/docs/runes#$effect-pre) under the hood. 70 | 71 | ### watchOnce 72 | 73 | In case you want to run the callback only once, you can use `watchOnce` and `watchOnce.pre`. It 74 | functions identically to the `watch` and `watch.pre` otherwise, but it does not accept any options 75 | object. 76 | -------------------------------------------------------------------------------- /sites/docs/src/lib/components/blueprint.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 10 | {@render children?.()} 11 | -------------------------------------------------------------------------------- /sites/docs/src/lib/components/demo-note.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | {@render children?.()} 9 |
10 | -------------------------------------------------------------------------------- /sites/docs/src/lib/components/demos/active-element.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 |

8 | Currently active element: 9 | 10 | {activeElement.current?.localName ?? "No active element found"} 11 | 12 |

13 |
14 | -------------------------------------------------------------------------------- /sites/docs/src/lib/components/demos/animation-frames.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 |
{stats}
34 |
45 | 53 | 54 |

55 | FPS limit: {fpsLimit}{fpsLimit === 0 ? " (not limited)" : ""} 56 |

57 | (fpsLimit = value[0] ?? 0)} 61 | min={0} 62 | max={144} 63 | /> 64 |
65 | 66 |

67 | Mouse sprite extracted from Animal Well 72 |

73 |
74 | -------------------------------------------------------------------------------- /sites/docs/src/lib/components/demos/debounced.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 |

12 | {#if debounced.current} 13 | You searched for: {debounced.current} 14 | {:else} 15 | Search for something above! 16 | {/if} 17 |

18 |
19 | -------------------------------------------------------------------------------- /sites/docs/src/lib/components/demos/element-rect.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 |