├── .github ├── CODEOWNERS ├── FUNDING.yml └── workflows │ ├── publish-landing.yml │ └── publish-npm-package.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── assets ├── aluma.png └── example.png ├── components.json ├── e2e └── demo.test.ts ├── eslint.config.js ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── postcss.config.js ├── src ├── app.css ├── app.d.ts ├── app.html ├── demo.spec.ts ├── lib │ ├── components │ │ ├── ui │ │ │ ├── button │ │ │ │ ├── button.svelte │ │ │ │ └── index.ts │ │ │ ├── calendar │ │ │ │ ├── calendar-cell.svelte │ │ │ │ ├── calendar-day.svelte │ │ │ │ ├── calendar-grid-body.svelte │ │ │ │ ├── calendar-grid-head.svelte │ │ │ │ ├── calendar-grid-row.svelte │ │ │ │ ├── calendar-grid.svelte │ │ │ │ ├── calendar-head-cell.svelte │ │ │ │ ├── calendar-header.svelte │ │ │ │ ├── calendar-heading.svelte │ │ │ │ ├── calendar-months.svelte │ │ │ │ ├── calendar-next-button.svelte │ │ │ │ ├── calendar-prev-button.svelte │ │ │ │ ├── calendar.svelte │ │ │ │ └── index.ts │ │ │ ├── checkbox │ │ │ │ ├── checkbox.svelte │ │ │ │ └── index.ts │ │ │ ├── dropdown-menu │ │ │ │ ├── dropdown-menu-checkbox-item.svelte │ │ │ │ ├── dropdown-menu-content.svelte │ │ │ │ ├── dropdown-menu-group-heading.svelte │ │ │ │ ├── dropdown-menu-item.svelte │ │ │ │ ├── dropdown-menu-label.svelte │ │ │ │ ├── dropdown-menu-radio-group.svelte │ │ │ │ ├── dropdown-menu-radio-item.svelte │ │ │ │ ├── dropdown-menu-separator.svelte │ │ │ │ ├── dropdown-menu-shortcut.svelte │ │ │ │ ├── dropdown-menu-sub-content.svelte │ │ │ │ ├── dropdown-menu-sub-trigger.svelte │ │ │ │ └── index.ts │ │ │ ├── input │ │ │ │ ├── index.ts │ │ │ │ └── input.svelte │ │ │ ├── label │ │ │ │ ├── index.ts │ │ │ │ └── label.svelte │ │ │ ├── popover │ │ │ │ ├── index.ts │ │ │ │ └── popover-content.svelte │ │ │ ├── range-calendar │ │ │ │ ├── index.ts │ │ │ │ ├── range-calendar-cell.svelte │ │ │ │ ├── range-calendar-day.svelte │ │ │ │ ├── range-calendar-grid-body.svelte │ │ │ │ ├── range-calendar-grid-head.svelte │ │ │ │ ├── range-calendar-grid-row.svelte │ │ │ │ ├── range-calendar-grid.svelte │ │ │ │ ├── range-calendar-head-cell.svelte │ │ │ │ ├── range-calendar-header.svelte │ │ │ │ ├── range-calendar-heading.svelte │ │ │ │ ├── range-calendar-months.svelte │ │ │ │ ├── range-calendar-next-button.svelte │ │ │ │ ├── range-calendar-prev-button.svelte │ │ │ │ └── range-calendar.svelte │ │ │ └── tooltip │ │ │ │ ├── index.ts │ │ │ │ └── tooltip-content.svelte │ │ └── utils.ts │ ├── index.ts │ ├── infinitable │ │ ├── context.ts │ │ ├── filters │ │ │ ├── index.ts │ │ │ ├── multi-select.svelte │ │ │ └── text.svelte │ │ ├── index.ts │ │ ├── infinitable-action-row.svelte │ │ ├── infinitable-filter-clear.svelte │ │ ├── infinitable-filter-icon.svelte │ │ ├── infinitable-header.svelte │ │ ├── infinitable-refresh.svelte │ │ ├── infinitable-row.svelte │ │ ├── infinitable-search.svelte │ │ ├── infinitable.svelte │ │ └── utils.svelte.ts │ └── types │ │ ├── filters.ts │ │ ├── handlers.ts │ │ ├── index.ts │ │ ├── refresh.ts │ │ ├── search.ts │ │ ├── sort.ts │ │ ├── table.ts │ │ └── utils.ts └── routes │ ├── +layout.svelte │ ├── +layout.ts │ ├── +page.svelte │ ├── example │ ├── data.ts │ ├── index.ts │ ├── service.ts │ ├── table.svelte │ ├── table.ts │ ├── types.ts │ └── utils.ts │ └── icons │ ├── github.svelte │ └── index.ts ├── static └── favicon.png ├── svelte.config.js ├── tailwind.config.ts ├── tsconfig.json └── vite.config.ts /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @adam-kov -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: adam-kov 2 | -------------------------------------------------------------------------------- /.github/workflows/publish-landing.yml: -------------------------------------------------------------------------------- 1 | name: Publish landing page 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | push: 7 | branches: 8 | - main 9 | paths: 10 | - 'src/**' 11 | 12 | jobs: 13 | publish: 14 | name: Publish to Cloudflare Pages 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | deployments: write 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Install pnpm 25 | uses: pnpm/action-setup@v4 26 | with: 27 | version: 9.15.3 28 | run_install: | 29 | - recursive: true 30 | args: [--frozen-lockfile, --strict-peer-dependencies] 31 | 32 | - name: Build 33 | run: pnpm build 34 | 35 | - name: Publish to Cloudflare Pages 36 | uses: cloudflare/wrangler-action@v3 37 | with: 38 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 39 | accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 40 | command: pages deploy ./.svelte-kit/cloudflare --project-name=svelte-infinitable 41 | -------------------------------------------------------------------------------- /.github/workflows/publish-npm-package.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npm registry 2 | on: 3 | workflow_dispatch: 4 | 5 | release: 6 | types: [published] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: 22 15 | - uses: pnpm/action-setup@v4 16 | with: 17 | version: 8.15.9 18 | run_install: | 19 | - recursive: true 20 | args: [--frozen-lockfile, --strict-peer-dependencies] 21 | - run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc 22 | - run: pnpm publish --no-git-checks --access public 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test-results 2 | node_modules 3 | 4 | # Output 5 | .output 6 | .vercel 7 | /.svelte-kit 8 | /build 9 | /dist 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Env 16 | .env 17 | .env.* 18 | !.env.example 19 | !.env.test 20 | 21 | # Vite 22 | vite.config.js.timestamp-* 23 | vite.config.ts.timestamp-* 24 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { 11 | "parser": "svelte" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Adevien 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Svelte Infinitable 2 | 3 | Svelte Infinitable is a virtual table component that uses native table elements and 4 | supports _infinite scrolling_, _searching_, _filtering_, _sorting_, and more. 5 | 6 | ![Example table](https://github.com/adevien-solutions/svelte-infinitable/blob/main/assets/example.png?raw=true) 7 | 8 | Live demo: [https://infinitable.adevien.com/](https://infinitable.adevien.com/) 9 | 10 | ## Sponsor 11 | 12 | [![Aluma](https://github.com/adevien-solutions/svelte-infinitable/blob/main/assets/aluma.png?raw=true)](https://aluma.io/?utm_source=svelte-infinitable&utm_medium=github&utm_campaign=sponsor) 13 | 14 | ## Installation 15 | 16 | Svelte Infinitable is built with Svelte 5, which means you need to have Svelte 5 installed in your project. 17 | 18 | ```bash 19 | pnpm add -D svelte-infinitable 20 | # or 21 | yarn add -D svelte-infinitable 22 | # or 23 | npm i -D svelte-infinitable 24 | ``` 25 | 26 | ## Usage 27 | 28 | > [!WARNING] 29 | > The package is yet to reach version 1.0.0, meaning breaking changes may be introduced with every minor version. 30 | 31 | ### Props 32 | 33 | | Name | Type | Default | Optional | Description | 34 | | -------------------- | --------------------------------------------- | ------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 35 | | `items` | `TableItem[]` | `[]` | Yes | The items to display in the table. If all of the items are loaded ahead of time, set `ignoreInfinite` to `true` and call `finishInitialLoad()` on the table instance when the initial load is completed. | 36 | | `rowHeight` | `number` | - | No | The height of each row in the table. | 37 | | `selectable` | `boolean` | `false` | Yes | Controls whether the table rows are selectable or not. | 38 | | `overscan` | `number` | `10` | Yes | The number of rows to render above and below the visible area of the table. Also controls how early the table will call the `onInfinite` function. | 39 | | `ignoreInfinite` | `boolean` | `false` | Yes | Controls whether the `onInfinite` function is called or not. Useful to set to `true` if you know that you are going to load all the data at once. If set to `true`, call `finishInitialLoad()` on the table instance when the initial load is completed. | 40 | | `rowDisabler` | `(item: TableItem, index: number) => boolean` | `undefined` | Yes | A function that takes an item and an index as parameters, and returns a boolean indicating whether the row at that index should be disabled or not. | 41 | | `disabledRowMessage` | `string` | `'This row cannot be selected'` | Yes | The text that will be displayed when the checkbox of a disabled row is hovered. | 42 | | `class` | `string` | `''` | Yes | Classes to apply to the table wrapper element. | 43 | | `style` | `string` | `''` | Yes | Style to apply to the table wrapper element. | 44 | | `debug` | `boolean` | `false` | Yes | If set to `true`, buttons will be rendered that make it easy to switch between different states of the table. | 45 | 46 | ### Events 47 | 48 | | Name | Type | Default | Optional | Description | 49 | | ------------ | ----------------- | ----------- | -------- | -------------------------------------------------------------------------- | 50 | | `onInfinite` | `InfiniteHandler` | `undefined` | Yes | Called when the table has reached the end of the loaded data. | 51 | | `onFilter` | `FilterHandler` | `undefined` | Yes | Called when the filtering of the table changes, and it's in `server` mode. | 52 | | `onSort` | `SortHandler` | `undefined` | Yes | Called when the sorting of the table changes, and it's in `server` mode. | 53 | | `onSelect` | `SelectHandler` | `undefined` | Yes | Called when the selected items change. | 54 | 55 | ### Snippets 56 | 57 | | Name | Argument | Description | 58 | | -------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | 59 | | `children` (default) | `{ item: TableItem; index: number; selectedCount: number; isAllSelected: boolean }` | A snippet for the table rows. | 60 | | `actions` | `{ selectedCount: number }` | A snippet above the table, but within it's border. It's the recommended place to mount the `Search`, `Refresh`, `FilterClear`, and any other custom components. | 61 | | `headers` | - | A snippet for the table headers. | 62 | | `loader` | - | A snippet to replace the loading indicator below existing rows. | 63 | | `completed` | - | A snippet to replace the message below existing rows, when the table has no more items. | 64 | | `empty` | - | A snippet to replace the message when the table has items, but they are filtered out. | 65 | | `loadingEmpty` | - | A snippet to replace the message when the table is loading and empty. | 66 | | `completedEmpty` | - | A snippet to replace the screen when the table has no more items and empty. | 67 | | `errorEmpty` | - | A snippet to replace the screen when the table has an error and empty. | 68 | | `idleEmpty` | - | A snippet to replace the screen when the table is idle and empty. | 69 | | `rowsDetail` | `{ rowCount: number; selectedCount: number }` | A snippet to replace the info message below the table. | 70 | | `error` | `{ message: string }` | A snippet to replace the error message below the table. | 71 | 72 | ## Example 73 | 74 | The following is a barebones example of using Svelte Infinitable. 75 | If you want to see a more complex one, check 76 | [`table.svelte` in the example direxctory](https://github.com/adevien-solutions/svelte-infinitable/blob/main/src/routes/example/table.svelte). 77 | 78 | ```svelte 79 | 102 | 103 | 104 | {#snippet headers()} 105 | {#each tableHeaders as header} 106 | 107 | {/each} 108 | {/snippet} 109 | 110 | {#snippet children({ index })} 111 | {@const { id, name, created_at } = items[index] ?? {}} 112 | {id} 113 | {name} 114 | {formatDateString(created_at)} 115 | {/snippet} 116 | 117 | ``` 118 | -------------------------------------------------------------------------------- /assets/aluma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adevien-solutions/svelte-infinitable/705384619423a4492697395f7759549a03f52a6e/assets/aluma.png -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adevien-solutions/svelte-infinitable/705384619423a4492697395f7759549a03f52a6e/assets/example.png -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://next.shadcn-svelte.com/schema.json", 3 | "style": "default", 4 | "tailwind": { 5 | "config": "tailwind.config.ts", 6 | "css": "src/app.css", 7 | "baseColor": "zinc" 8 | }, 9 | "aliases": { 10 | "components": "$lib/components", 11 | "utils": "$lib/components/utils", 12 | "ui": "$lib/components/ui", 13 | "hooks": "$lib/hooks" 14 | }, 15 | "typescript": true, 16 | "registry": "https://next.shadcn-svelte.com/registry" 17 | } 18 | -------------------------------------------------------------------------------- /e2e/demo.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('home page has expected h1', async ({ page }) => { 4 | await page.goto('/'); 5 | await expect(page.locator('h1')).toBeVisible(); 6 | }); 7 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import js from '@eslint/js'; 3 | import svelte from 'eslint-plugin-svelte'; 4 | import globals from 'globals'; 5 | import ts from 'typescript-eslint'; 6 | 7 | export default ts.config( 8 | js.configs.recommended, 9 | ...ts.configs.recommended, 10 | ...svelte.configs['flat/recommended'], 11 | prettier, 12 | ...svelte.configs['flat/prettier'], 13 | { 14 | languageOptions: { 15 | globals: { 16 | ...globals.browser, 17 | ...globals.node 18 | } 19 | } 20 | }, 21 | { 22 | files: ['**/*.svelte'], 23 | 24 | languageOptions: { 25 | parserOptions: { 26 | parser: ts.parser 27 | } 28 | } 29 | }, 30 | { 31 | ignores: ['build/', '.svelte-kit/', 'dist/'] 32 | } 33 | ); 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-infinitable", 3 | "version": "0.0.14", 4 | "scripts": { 5 | "dev": "vite dev --open", 6 | "build": "vite build", 7 | "preview": "vite preview", 8 | "package": "svelte-kit sync && svelte-package && publint", 9 | "prepublishOnly": "npm run package", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 12 | "format": "prettier --write .", 13 | "lint": "prettier --check . && eslint .", 14 | "test:unit": "vitest", 15 | "test": "npm run test:unit -- --run && npm run test:e2e", 16 | "test:e2e": "playwright test" 17 | }, 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/adevien-solutions/svelte-infinitable" 22 | }, 23 | "homepage": "https://infinitable.adevien.com/", 24 | "files": [ 25 | "dist", 26 | "!dist/**/*.test.*", 27 | "!dist/**/*.spec.*" 28 | ], 29 | "sideEffects": [ 30 | "**/*.css" 31 | ], 32 | "svelte": "./dist/index.js", 33 | "types": "./dist/index.d.ts", 34 | "type": "module", 35 | "exports": { 36 | ".": { 37 | "types": "./dist/index.d.ts", 38 | "svelte": "./dist/index.js" 39 | }, 40 | "./types": { 41 | "types": "./dist/types/index.d.ts", 42 | "svelte": "./dist/types/index.js" 43 | }, 44 | "./types/*": { 45 | "types": "./dist/types/index.d.ts", 46 | "svelte": "./dist/types/index.js", 47 | "default": "./dist/types/index.js" 48 | } 49 | }, 50 | "peerDependencies": { 51 | "svelte": "^5.0.0" 52 | }, 53 | "devDependencies": { 54 | "@internationalized/date": "^3.5.6", 55 | "@playwright/test": "^1.45.3", 56 | "@sveltejs/adapter-cloudflare": "^5.0.0", 57 | "@sveltejs/kit": "^2.15.2", 58 | "@sveltejs/package": "^2.0.0", 59 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 60 | "@tailwindcss/typography": "^0.5.15", 61 | "@types/eslint": "^9.6.0", 62 | "autoprefixer": "^10.4.20", 63 | "bits-ui": "1.0.0-next.72", 64 | "clsx": "^2.1.1", 65 | "eslint": "^9.7.0", 66 | "eslint-config-prettier": "^9.1.0", 67 | "eslint-plugin-svelte": "^2.36.0", 68 | "globals": "^15.0.0", 69 | "lucide-svelte": "^0.456.0", 70 | "mdsvex": "^0.11.2", 71 | "prettier": "^3.3.2", 72 | "prettier-plugin-svelte": "^3.2.6", 73 | "prettier-plugin-tailwindcss": "^0.6.5", 74 | "publint": "^0.2.0", 75 | "svelte": "^5.16.5", 76 | "svelte-check": "^4.0.0", 77 | "tailwind-merge": "^2.5.5", 78 | "tailwind-variants": "^0.3.0", 79 | "tailwindcss": "^3.4.9", 80 | "tailwindcss-animate": "^1.0.7", 81 | "typescript": "^5.0.0", 82 | "typescript-eslint": "^8.0.0", 83 | "vite": "^6.0.7", 84 | "vitest": "^2.0.4" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@playwright/test'; 2 | 3 | export default defineConfig({ 4 | webServer: { 5 | command: 'npm run build && npm run preview', 6 | port: 4173 7 | }, 8 | 9 | testDir: 'e2e' 10 | }); 11 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --muted: 240 4.8% 95.9%; 10 | --muted-foreground: 240 3.8% 46.1%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 240 10% 3.9%; 15 | --border: 240 5.9% 90%; 16 | --input: 240 5.9% 90%; 17 | --primary: 240 5.9% 10%; 18 | --primary-foreground: 0 0% 98%; 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | --accent: 240 4.8% 95.9%; 22 | --accent-foreground: 240 5.9% 10%; 23 | --destructive: 0 72.2% 50.6%; 24 | --destructive-foreground: 0 0% 98%; 25 | --ring: 240 10% 3.9%; 26 | --radius: 0.5rem; 27 | --sidebar-background: 0 0% 98%; 28 | --sidebar-foreground: 240 5.3% 26.1%; 29 | --sidebar-primary: 240 5.9% 10%; 30 | --sidebar-primary-foreground: 0 0% 98%; 31 | --sidebar-accent: 240 4.8% 95.9%; 32 | --sidebar-accent-foreground: 240 5.9% 10%; 33 | --sidebar-border: 220 13% 91%; 34 | --sidebar-ring: 217.2 91.2% 59.8%; 35 | } 36 | 37 | .dark { 38 | --background: 240 10% 3.9%; 39 | --foreground: 0 0% 98%; 40 | --muted: 240 3.7% 15.9%; 41 | --muted-foreground: 240 5% 64.9%; 42 | --popover: 240 10% 3.9%; 43 | --popover-foreground: 0 0% 98%; 44 | --card: 240 10% 3.9%; 45 | --card-foreground: 0 0% 98%; 46 | --border: 240 3.7% 15.9%; 47 | --input: 240 3.7% 15.9%; 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 240 5.9% 10%; 50 | --secondary: 240 3.7% 15.9%; 51 | --secondary-foreground: 0 0% 98%; 52 | --accent: 240 3.7% 15.9%; 53 | --accent-foreground: 0 0% 98%; 54 | --destructive: 0 62.8% 30.6%; 55 | --destructive-foreground: 0 0% 98%; 56 | --ring: 240 4.9% 83.9%; 57 | --sidebar-background: 240 5.9% 10%; 58 | --sidebar-foreground: 240 4.8% 95.9%; 59 | --sidebar-primary: 224.3 76.3% 48%; 60 | --sidebar-primary-foreground: 0 0% 100%; 61 | --sidebar-accent: 240 3.7% 15.9%; 62 | --sidebar-accent-foreground: 240 4.8% 95.9%; 63 | --sidebar-border: 240 3.7% 15.9%; 64 | --sidebar-ring: 217.2 91.2% 59.8%; 65 | } 66 | } 67 | 68 | @layer base { 69 | * { 70 | @apply border-border; 71 | } 72 | body { 73 | @apply bg-background text-foreground; 74 | } 75 | } -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/demo.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/button/button.svelte: -------------------------------------------------------------------------------- 1 | 39 | 40 | 54 | 55 | {#if href} 56 | 57 | {@render children?.()} 58 | 59 | {:else} 60 | 68 | {/if} 69 | -------------------------------------------------------------------------------- /src/lib/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import Root, { 2 | type ButtonProps, 3 | type ButtonSize, 4 | type ButtonVariant, 5 | buttonVariants, 6 | } from "./button.svelte"; 7 | 8 | export { 9 | Root, 10 | type ButtonProps as Props, 11 | // 12 | Root as Button, 13 | buttonVariants, 14 | type ButtonProps, 15 | type ButtonSize, 16 | type ButtonVariant, 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-cell.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-day.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 31 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-grid-body.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-grid-head.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-grid-row.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-grid.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-head-cell.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-header.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-heading.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-months.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-next-button.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | {#snippet Fallback()} 16 | 17 | {/snippet} 18 | 19 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-prev-button.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | {#snippet Fallback()} 16 | 17 | {/snippet} 18 | 19 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 20 | 28 | {#snippet children({ months, weekdays })} 29 | 30 | 31 | 32 | 33 | 34 | 35 | {#each months as month} 36 | 37 | 38 | 39 | {#each weekdays as weekday} 40 | 41 | {weekday.slice(0, 2)} 42 | 43 | {/each} 44 | 45 | 46 | 47 | {#each month.weeks as weekDates} 48 | 49 | {#each weekDates as date} 50 | 51 | 52 | 53 | {/each} 54 | 55 | {/each} 56 | 57 | 58 | {/each} 59 | 60 | {/snippet} 61 | 62 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./calendar.svelte"; 2 | import Cell from "./calendar-cell.svelte"; 3 | import Day from "./calendar-day.svelte"; 4 | import Grid from "./calendar-grid.svelte"; 5 | import Header from "./calendar-header.svelte"; 6 | import Months from "./calendar-months.svelte"; 7 | import GridRow from "./calendar-grid-row.svelte"; 8 | import Heading from "./calendar-heading.svelte"; 9 | import GridBody from "./calendar-grid-body.svelte"; 10 | import GridHead from "./calendar-grid-head.svelte"; 11 | import HeadCell from "./calendar-head-cell.svelte"; 12 | import NextButton from "./calendar-next-button.svelte"; 13 | import PrevButton from "./calendar-prev-button.svelte"; 14 | 15 | export { 16 | Day, 17 | Cell, 18 | Grid, 19 | Header, 20 | Months, 21 | GridRow, 22 | Heading, 23 | GridBody, 24 | GridHead, 25 | HeadCell, 26 | NextButton, 27 | PrevButton, 28 | // 29 | Root as Calendar, 30 | }; 31 | -------------------------------------------------------------------------------- /src/lib/components/ui/checkbox/checkbox.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | {#snippet children({ checked, indeterminate })} 27 |
28 | {#if indeterminate} 29 | 30 | {:else} 31 | 32 | {/if} 33 |
34 | {/snippet} 35 |
36 | -------------------------------------------------------------------------------- /src/lib/components/ui/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./checkbox.svelte"; 2 | export { 3 | Root, 4 | // 5 | Root as Checkbox, 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | {#snippet children({ checked, indeterminate })} 31 | 32 | {#if indeterminate} 33 | 34 | {:else} 35 | 36 | {/if} 37 | 38 | {@render childrenProp?.()} 39 | {/snippet} 40 | 41 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 26 | 27 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
22 | {@render children?.()} 23 |
24 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | {#snippet children({ checked })} 23 | 24 | {#if checked} 25 | 26 | {/if} 27 | 28 | {@render childrenProp?.({ checked })} 29 | {/snippet} 30 | 31 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | {@render children?.()} 20 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | {@render children?.()} 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/index.ts: -------------------------------------------------------------------------------- 1 | import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; 2 | import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; 3 | import Content from "./dropdown-menu-content.svelte"; 4 | import GroupHeading from "./dropdown-menu-group-heading.svelte"; 5 | import Item from "./dropdown-menu-item.svelte"; 6 | import Label from "./dropdown-menu-label.svelte"; 7 | import RadioItem from "./dropdown-menu-radio-item.svelte"; 8 | import Separator from "./dropdown-menu-separator.svelte"; 9 | import Shortcut from "./dropdown-menu-shortcut.svelte"; 10 | import SubContent from "./dropdown-menu-sub-content.svelte"; 11 | import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; 12 | 13 | const Sub = DropdownMenuPrimitive.Sub; 14 | const Root = DropdownMenuPrimitive.Root; 15 | const Trigger = DropdownMenuPrimitive.Trigger; 16 | const Group = DropdownMenuPrimitive.Group; 17 | const RadioGroup = DropdownMenuPrimitive.RadioGroup; 18 | 19 | export { 20 | CheckboxItem, 21 | Content, 22 | Root as DropdownMenu, 23 | CheckboxItem as DropdownMenuCheckboxItem, 24 | Content as DropdownMenuContent, 25 | Group as DropdownMenuGroup, 26 | GroupHeading as DropdownMenuGroupHeading, 27 | Item as DropdownMenuItem, 28 | Label as DropdownMenuLabel, 29 | RadioGroup as DropdownMenuRadioGroup, 30 | RadioItem as DropdownMenuRadioItem, 31 | Separator as DropdownMenuSeparator, 32 | Shortcut as DropdownMenuShortcut, 33 | Sub as DropdownMenuSub, 34 | SubContent as DropdownMenuSubContent, 35 | SubTrigger as DropdownMenuSubTrigger, 36 | Trigger as DropdownMenuTrigger, 37 | Group, 38 | GroupHeading, 39 | Item, 40 | Label, 41 | RadioGroup, 42 | RadioItem, 43 | Root, 44 | Separator, 45 | Shortcut, 46 | Sub, 47 | SubContent, 48 | SubTrigger, 49 | Trigger, 50 | }; 51 | -------------------------------------------------------------------------------- /src/lib/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./input.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Input, 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/input/input.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./label.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Label, 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/label/label.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/popover/index.ts: -------------------------------------------------------------------------------- 1 | import { Popover as PopoverPrimitive } from "bits-ui"; 2 | import Content from "./popover-content.svelte"; 3 | const Root = PopoverPrimitive.Root; 4 | const Trigger = PopoverPrimitive.Trigger; 5 | const Close = PopoverPrimitive.Close; 6 | 7 | export { 8 | Root, 9 | Content, 10 | Trigger, 11 | Close, 12 | // 13 | Root as Popover, 14 | Content as PopoverContent, 15 | Trigger as PopoverTrigger, 16 | Close as PopoverClose, 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/popover/popover-content.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 28 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/range-calendar/index.ts: -------------------------------------------------------------------------------- 1 | import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui"; 2 | import Root from "./range-calendar.svelte"; 3 | import Cell from "./range-calendar-cell.svelte"; 4 | import Day from "./range-calendar-day.svelte"; 5 | import Grid from "./range-calendar-grid.svelte"; 6 | import Header from "./range-calendar-header.svelte"; 7 | import Months from "./range-calendar-months.svelte"; 8 | import GridRow from "./range-calendar-grid-row.svelte"; 9 | import Heading from "./range-calendar-heading.svelte"; 10 | import HeadCell from "./range-calendar-head-cell.svelte"; 11 | import NextButton from "./range-calendar-next-button.svelte"; 12 | import PrevButton from "./range-calendar-prev-button.svelte"; 13 | 14 | const GridHead = RangeCalendarPrimitive.GridHead; 15 | const GridBody = RangeCalendarPrimitive.GridBody; 16 | 17 | export { 18 | Day, 19 | Cell, 20 | Grid, 21 | Header, 22 | Months, 23 | GridRow, 24 | Heading, 25 | GridBody, 26 | GridHead, 27 | HeadCell, 28 | NextButton, 29 | PrevButton, 30 | // 31 | Root as RangeCalendar, 32 | }; 33 | -------------------------------------------------------------------------------- /src/lib/components/ui/range-calendar/range-calendar-cell.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/range-calendar/range-calendar-day.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 35 | -------------------------------------------------------------------------------- /src/lib/components/ui/range-calendar/range-calendar-grid-body.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/range-calendar/range-calendar-grid-head.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/range-calendar/range-calendar-grid-row.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/range-calendar/range-calendar-grid.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/range-calendar/range-calendar-head-cell.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/range-calendar/range-calendar-header.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/range-calendar/range-calendar-heading.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/range-calendar/range-calendar-months.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /src/lib/components/ui/range-calendar/range-calendar-next-button.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | {#snippet Fallback()} 16 | 17 | {/snippet} 18 | 19 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/range-calendar/range-calendar-prev-button.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | {#snippet Fallback()} 16 | 17 | {/snippet} 18 | 19 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/range-calendar/range-calendar.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | {#snippet children({ months, weekdays })} 25 | 26 | 27 | 28 | 29 | 30 | 31 | {#each months as month} 32 | 33 | 34 | 35 | {#each weekdays as weekday} 36 | 37 | {weekday.slice(0, 2)} 38 | 39 | {/each} 40 | 41 | 42 | 43 | {#each month.weeks as weekDates} 44 | 45 | {#each weekDates as date} 46 | 47 | 48 | 49 | {/each} 50 | 51 | {/each} 52 | 53 | 54 | {/each} 55 | 56 | {/snippet} 57 | 58 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | import { Tooltip as TooltipPrimitive } from "bits-ui"; 2 | import Content from "./tooltip-content.svelte"; 3 | 4 | const Root = TooltipPrimitive.Root; 5 | const Trigger = TooltipPrimitive.Trigger; 6 | const Provider = TooltipPrimitive.Provider; 7 | 8 | export { 9 | Root, 10 | Trigger, 11 | Content, 12 | Provider, 13 | // 14 | Root as Tooltip, 15 | Content as TooltipContent, 16 | Trigger as TooltipTrigger, 17 | Provider as TooltipProvider, 18 | }; 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/tooltip-content.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /src/lib/components/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './infinitable/index.js'; 2 | -------------------------------------------------------------------------------- /src/lib/infinitable/context.ts: -------------------------------------------------------------------------------- 1 | import { getContext, setContext } from 'svelte'; 2 | import type { Readable } from 'svelte/store'; 3 | import type { 4 | FilterDetailItem, 5 | InternalSortDetail, 6 | RefreshHandler, 7 | SearchHandler, 8 | TableFilterHeader, 9 | TableHeaderStyle, 10 | TableSearchSettings 11 | } from '../types/index.js'; 12 | 13 | const infiniteTableContextKey = Symbol('infinite_table_context_key'); 14 | 15 | export type InfiniteTableRowData = { selected: boolean; meta?: Record }; 16 | export type InfiniteTableRowDataStoreValue = Record; 17 | 18 | export type InfiniteTableState = 'idle' | 'loading' | 'completed' | 'error'; 19 | 20 | export type InfiniteTableContext = { 21 | state: Readable; 22 | element: { 23 | table: Readable; 24 | }; 25 | /** Controls whether the table rows are selectable or not. */ 26 | selectable: boolean; 27 | selectedCount: () => number; 28 | rowCount: () => number; 29 | isAllSelected: () => boolean; 30 | sorting: Readable; 31 | onHeaderMount: (style: TableHeaderStyle) => void; 32 | onHeaderDestroy: (style: TableHeaderStyle) => void; 33 | onFilterMount: (filterHeader: TableFilterHeader) => void; 34 | onFilterChange: (detail: FilterDetailItem, isUserReset: boolean) => void; 35 | onFilterDestroy: (filterHeader: TableFilterHeader) => void; 36 | onSortChange: (detail: InternalSortDetail) => void; 37 | refresh: (handler?: RefreshHandler) => void; 38 | onRefreshMount: (resetFunction: () => void) => void; 39 | onRefreshDestroy: (resetFunction: () => void) => void; 40 | allFiltersDefault: Readable; 41 | clearFilters: () => Promise; 42 | onSearchChange: (value: string, settings: TableSearchSettings, handler?: SearchHandler) => void; 43 | onSearchDestroy: () => void; 44 | resetFlag: Readable; 45 | }; 46 | 47 | export function setInfiniteTableContext(ctx: InfiniteTableContext): InfiniteTableContext { 48 | return setContext(infiniteTableContextKey, ctx); 49 | } 50 | 51 | export function getInfiniteTableContext(): InfiniteTableContext { 52 | return getContext(infiniteTableContextKey); 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/infinitable/filters/index.ts: -------------------------------------------------------------------------------- 1 | import MultiSelect from './multi-select.svelte'; 2 | import Text from './text.svelte'; 3 | 4 | export { MultiSelect, Text }; 5 | -------------------------------------------------------------------------------- /src/lib/infinitable/filters/multi-select.svelte: -------------------------------------------------------------------------------- 1 | 61 | 62 |
63 |
66 | 70 | 73 |
74 |
75 | {#each internalOption ?? [] as option} 76 | 80 | {/each} 81 |
82 |
83 | -------------------------------------------------------------------------------- /src/lib/infinitable/filters/text.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 21 |
22 | -------------------------------------------------------------------------------- /src/lib/infinitable/index.ts: -------------------------------------------------------------------------------- 1 | import ActionRow from './infinitable-action-row.svelte'; 2 | import FilterClear from './infinitable-filter-clear.svelte'; 3 | import Header from './infinitable-header.svelte'; 4 | import Refresh from './infinitable-refresh.svelte'; 5 | import Search from './infinitable-search.svelte'; 6 | import Root from './infinitable.svelte'; 7 | 8 | export { ActionRow, FilterClear, Header, Refresh, Root, Search }; 9 | -------------------------------------------------------------------------------- /src/lib/infinitable/infinitable-action-row.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | {@render children?.()} 15 |
16 | -------------------------------------------------------------------------------- /src/lib/infinitable/infinitable-filter-clear.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 35 | -------------------------------------------------------------------------------- /src/lib/infinitable/infinitable-filter-icon.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | {#if showBadge} 16 | 20 | {/if} 21 | 22 | -------------------------------------------------------------------------------- /src/lib/infinitable/infinitable-header.svelte: -------------------------------------------------------------------------------- 1 | 259 | 260 | 268 |
269 | {#if header.sort} 270 | 271 | 272 | 273 | {#snippet child({ props })} 274 | 288 | {/snippet} 289 | 290 | 291 |

292 | Sort 293 | {#if $sorting?.id === id} 294 | {#if $sorting?.direction === 'asc'} 295 | descending 296 | {:else} 297 | ascending 298 | {/if} 299 | {:else if header.sort} 300 | {header.sort.defaultDirection === 'asc' ? 'ascending' : 'descending'} 301 | {/if} 302 | by {header.label.toLowerCase()} 303 |

304 |
305 |
306 |
307 | {:else} 308 | 309 | {header.label} 310 | 311 | {/if} 312 | {#if isFilterHeader(header) && filter} 313 | 314 | 315 | {#snippet child(popover)} 316 | 317 | 318 | 319 | {#snippet child(tooltip)} 320 | 327 | {/snippet} 328 | 329 | 330 |

Open filter

331 |
332 |
333 |
334 | {/snippet} 335 |
336 | 337 | 338 |
339 |
340 | {#if children} 341 | {@render children()} 342 | {:else if header.filter.type === 'text'} 343 | 344 | {:else if header.filter.type === 'multiSelect'} 345 | 346 | {/if} 347 |
348 | 349 |
350 | 356 | Reset 357 | 358 | 363 | Save 364 | 365 |
366 |
367 |
368 |
369 | {/if} 370 |
371 | 372 | -------------------------------------------------------------------------------- /src/lib/infinitable/infinitable-refresh.svelte: -------------------------------------------------------------------------------- 1 | 59 | 60 | 61 | 62 | 63 | {#snippet child({ props })} 64 | 79 | {/snippet} 80 | 81 | 82 | {#if tooltip} 83 | {@render tooltip(lastRefresh)} 84 | {:else} 85 |

86 | Refreshed {lastRefresh?.value} 87 |

88 | {/if} 89 |
90 |
91 |
92 | -------------------------------------------------------------------------------- /src/lib/infinitable/infinitable-row.svelte: -------------------------------------------------------------------------------- 1 | 46 | 47 | th]:py-2 [&>th]:pl-2 last:[&>th]:pr-2', 51 | '[&>td]:py-1 [&>td]:pl-2 last:[&>td]:pr-2', 52 | header 53 | ? '' 54 | : selected 55 | ? 'bg-blue-50 hover:bg-blue-100/80 focus-visible:bg-blue-100/80' 56 | : 'hover:bg-gray-100 focus-visible:bg-gray-100', 57 | c 58 | )} 59 | {...rest} 60 | > 61 | {#if selectable} 62 | 63 | {#if disabled} 64 | {#if disabledMessage} 65 | 66 | 67 | 68 | {@render disabledCheckbox()} 69 | 70 | 71 |

{disabledMessage}

72 |
73 |
74 |
75 | {:else} 76 | {@render disabledCheckbox()} 77 | {/if} 78 | {:else} 79 | 86 | {/if} 87 |
88 | {/if} 89 | {@render children?.()} 90 | 91 | 92 | {#snippet disabledCheckbox()} 93 | 94 | {/snippet} 95 | -------------------------------------------------------------------------------- /src/lib/infinitable/infinitable-search.svelte: -------------------------------------------------------------------------------- 1 | 53 | 54 |
55 | 73 |
74 | -------------------------------------------------------------------------------- /src/lib/infinitable/infinitable.svelte: -------------------------------------------------------------------------------- 1 | 711 | 712 | {#if debug} 713 |
714 | 723 | 732 | 741 | 750 |
751 | {/if} 752 |
753 | {@render actions?.({ selectedCount })} 754 |
755 | 759 | 760 | 761 | {@render headers?.()} 762 | 763 | 764 | 770 | {#each internalItems as { data, index }, position} 771 | {#if position >= visibleRowIndex.start && position < visibleRowIndex.end} 772 | {#key flag} 773 | onSelectorChange(isSelected, data, index)} 775 | selected={selected.has(data)} 776 | disabled={rowDisabler?.(data, index) ?? false} 777 | disabledMessage={disabledRowMessage} 778 | > 779 | {@render children?.({ 780 | item: data, 781 | index, 782 | selectedCount, 783 | isAllSelected 784 | })} 785 | 786 | {/key} 787 | {/if} 788 | {#if position === internalItems.length - 1} 789 | 790 | 815 | 816 | {/if} 817 | {/each} 818 | 819 |
791 | {#if $internalState === 'loading'} 792 | {#if loader} 793 | {@render loader?.()} 794 | {:else} 795 |
798 | 799 | Loading 800 |
801 | {/if} 802 | {/if} 803 | {#if $internalState === 'completed'} 804 | {#if completed} 805 | {@render completed?.()} 806 | {:else} 807 |
810 | No more items to load 811 |
812 | {/if} 813 | {/if} 814 |
820 | {#if rowCount === 0 && items.length > 0} 821 | 822 | {#if empty} 823 | {@render empty?.()} 824 | {:else} 825 |
830 | 831 |

No items found

832 |
833 | {/if} 834 | {:else if items.length === 0} 835 |
840 | {#if $internalState === 'loading' || isInitialLoad || !isMounted} 841 | 842 | {#if loadingEmpty} 843 | {@render loadingEmpty?.()} 844 | {:else} 845 | 846 |

Loading

847 | {/if} 848 | {:else if $internalState === 'completed'} 849 | 850 | {#if completedEmpty} 851 | {@render completedEmpty?.()} 852 | {:else} 853 | 854 |

No items to display

855 | {/if} 856 | {:else if $internalState === 'error'} 857 | 858 | {#if errorEmpty} 859 | {@render errorEmpty?.()} 860 | {:else} 861 | 862 |

{errorMessage || 'An unknown error occurred'}

863 | {/if} 864 | {:else} 865 | 866 | {#if idleEmpty} 867 | {@render idleEmpty?.()} 868 | {:else} 869 | 870 |

No items to display

871 | {/if} 872 | {/if} 873 |
874 | {/if} 875 |
876 |
877 | 878 | 879 | {#if rowsDetail} 880 | {@render rowsDetail?.({ rowCount, selectedCount })} 881 | {:else} 882 |

883 | {rowCount} row{rowCount === 1 ? '' : 's'} shown{#if selectable}, {selectedCount} selected{/if} 884 |

885 | {/if} 886 | 887 | 888 | {#if errorMessage} 889 | {#if error} 890 | {@render error?.({ message: errorMessage })} 891 | {:else} 892 |
893 | {errorMessage} 894 |
895 | {/if} 896 | {/if} 897 | -------------------------------------------------------------------------------- /src/lib/infinitable/utils.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CustomFilter, 3 | TableFilterHeader, 4 | TableHeader, 5 | TableSearchSettings, 6 | TextFilter 7 | } from '../types/index.js'; 8 | import type { LastRefreshDetail, RefreshLabelUpdateTickRate } from '../types/refresh.js'; 9 | 10 | export function debounce unknown>(fn: T, delay: number) { 11 | let timeout: NodeJS.Timeout | undefined; 12 | 13 | return function (this: ThisParameterType, ...args: Parameters) { 14 | clearTimeout(timeout); 15 | timeout = setTimeout(() => fn.apply(this, args), delay); 16 | } as T; 17 | } 18 | 19 | export function uniqueId(prefix = '') { 20 | return `${prefix}${Math.random().toString(36).substring(2, 9)}`; 21 | } 22 | 23 | export function isFilterHeader(header: TableHeader): header is TableFilterHeader { 24 | return ( 25 | header.filter !== undefined && 26 | 'type' in header.filter && 27 | (header.filter.type === 'text' || 28 | header.filter.type === 'multiSelect' || 29 | header.filter.type === 'custom') 30 | ); 31 | } 32 | 33 | export function searchSettingsToFilter(settings: TableSearchSettings | undefined, value: string) { 34 | const searchTerm = value.trim(); 35 | if (!(settings && searchTerm) || settings.mode === 'server') { 36 | return; 37 | } 38 | 39 | if (settings.mode === 'custom') { 40 | return { 41 | type: 'custom', 42 | mode: 'custom', 43 | value: searchTerm, 44 | isDefault: searchTerm === '', 45 | onFilter: settings.onSearch, 46 | isDefaultValue: (value: unknown) => value === '' 47 | } satisfies CustomFilter & { isDefault: boolean }; 48 | } 49 | 50 | return { 51 | type: 'text', 52 | mode: 'auto', 53 | property: settings.property, 54 | value: searchTerm, 55 | isDefault: searchTerm === '', 56 | settings: { 57 | caseSensitive: settings.caseSensitive 58 | } 59 | } satisfies TextFilter & { isDefault: boolean }; 60 | } 61 | 62 | export function formatRelativeTime(date: Date): string { 63 | const diff = Date.now() - date.getTime(); 64 | const seconds = Math.floor(diff / 1000); 65 | const minutes = Math.floor(seconds / 60); 66 | const hours = Math.floor(minutes / 60); 67 | const days = Math.floor(hours / 24); 68 | 69 | if (seconds < 60) { 70 | return 'just now'; 71 | } 72 | 73 | if (minutes < 60) { 74 | return `${minutes} minute${minutes === 1 ? '' : 's'} ago`; 75 | } 76 | 77 | if (hours < 24) { 78 | return `${hours} hour${hours === 1 ? '' : 's'} ago`; 79 | } 80 | 81 | if (days < 7) { 82 | return `${days} day${days === 1 ? '' : 's'} ago`; 83 | } 84 | 85 | return date.toLocaleDateString(); 86 | } 87 | 88 | export function createTickingRelativeTime( 89 | date: Date, 90 | tickRate: RefreshLabelUpdateTickRate = 'minute' 91 | ): LastRefreshDetail { 92 | const ticker = $state({ 93 | refreshedAt: date, 94 | value: formatRelativeTime(date), 95 | cancel: () => {} 96 | }); 97 | const interval = setInterval( 98 | () => { 99 | ticker.value = formatRelativeTime(date); 100 | }, 101 | tickRate === 'second' ? 1000 : 60000 102 | ); 103 | ticker.cancel = () => clearInterval(interval); 104 | return ticker; 105 | } 106 | -------------------------------------------------------------------------------- /src/lib/types/filters.ts: -------------------------------------------------------------------------------- 1 | import type { TableHeader, TableItem } from './table.js'; 2 | import type { AnyRecord, WithRequired } from './utils.js'; 3 | 4 | /** 5 | * A custom filtering function that gets called on each item in the table. 6 | * @returns `true` if the item should be shown, `false` if it should be hidden. 7 | */ 8 | export type CustomFiltering = (item: T, index: number) => boolean; 9 | 10 | type TableFilterModeOption = 'server' | 'auto' | 'custom'; 11 | type TableFilterMode = { 12 | /** 13 | * If the mode of the filter is `server`, you'll have to handle it on the server side. 14 | * To do this, provide the `onFilter` function for the table. 15 | * 16 | * Setting this to anything other than `server` will make the filter client sided. 17 | * 18 | * __WARNING:__ Having client side filtering with infinite loading enabled can trigger many `infinite` events. 19 | */ 20 | mode: T; 21 | }; 22 | type GenericFilter = T; 23 | 24 | export type FilterOption = { 25 | /** The text that will appear to users. */ 26 | label: string; 27 | /** The value that will be used in the filter. */ 28 | name: string; 29 | }; 30 | 31 | /** A free text filter. */ 32 | export type TextFilter = GenericFilter< 33 | { 34 | type: 'text'; 35 | value?: string; 36 | placeholder?: string; 37 | } & ( 38 | | TableFilterMode<'server'> 39 | | (TableFilterMode<'auto'> & { 40 | property: string | number | symbol | (string | number | symbol)[]; 41 | settings?: { 42 | /** 43 | * If true, the filter will be case sensitive. 44 | * @default false 45 | */ 46 | caseSensitive?: boolean; 47 | }; 48 | }) 49 | | (TableFilterMode<'custom'> & { 50 | isDefaultValue: (value: string) => boolean; 51 | onFilter: CustomFiltering; 52 | }) 53 | ) 54 | >; 55 | 56 | /** A multi-select filter. */ 57 | export type MultiSelectFilter = GenericFilter< 58 | { 59 | type: 'multiSelect'; 60 | options: T[]; 61 | value?: T[]; 62 | } & ( 63 | | TableFilterMode<'server'> 64 | | (TableFilterMode<'auto'> & { 65 | property: string | number | symbol | (string | number | symbol)[]; 66 | }) 67 | | (TableFilterMode<'custom'> & { 68 | isDefaultValue: (value: T[]) => boolean; 69 | onFilter: CustomFiltering; 70 | }) 71 | ) 72 | >; 73 | 74 | /** A custom filter. */ 75 | export type CustomFilter = GenericFilter< 76 | { 77 | type: 'custom'; 78 | value?: T; 79 | } & ( 80 | | TableFilterMode<'server'> 81 | | (TableFilterMode<'custom'> & { 82 | isDefaultValue: (value: T) => boolean; 83 | onFilter: CustomFiltering; 84 | }) 85 | ) 86 | >; 87 | 88 | export type TableHeaderFilter = TextFilter | MultiSelectFilter | CustomFilter; 89 | 90 | export type TableFilterHeader = WithRequired< 91 | TableHeader, 92 | 'filter' 93 | >; 94 | -------------------------------------------------------------------------------- /src/lib/types/handlers.ts: -------------------------------------------------------------------------------- 1 | import type { SortDirection } from './sort.js'; 2 | import type { TableHeader, TableItem } from './table.js'; 3 | import type { WithRequired } from './utils.js'; 4 | 5 | export type RefreshDetail = { 6 | /** 7 | * Should be called when the items are loaded, but there are more items to load. 8 | * @param items The items that are going to replace the current list. 9 | */ 10 | loaded: (items: TableItem[]) => void; 11 | /** 12 | * Should be called when the items are loaded, and there are no more items to load. 13 | * @param items The items that are going to replace the current list. 14 | */ 15 | completed: (items: TableItem[]) => void; 16 | error: (message?: string) => void; 17 | }; 18 | export type RefreshHandler = (result: RefreshDetail) => Promise | void; 19 | 20 | export type InfiniteDetail = { 21 | /** 22 | * Should be called when the items are loaded, but there are more items to load. 23 | * @param newItems The items that are going to be appended to the end of the current list. 24 | */ 25 | loaded: (newItems: TableItem[]) => void; 26 | /** 27 | * Should be called when the items are loaded, and there are no more items to load. 28 | * @param newItems The items that are going to be appended to the end of the current list. 29 | */ 30 | completed: (newItems: TableItem[]) => void; 31 | error: (message?: string) => void; 32 | }; 33 | export type InfiniteHandler = (result: InfiniteDetail) => Promise | void; 34 | 35 | export type SearchDetail = { 36 | /** The value of the search input. */ 37 | value: string; 38 | }; 39 | /** 40 | * A function that gets called when the user types in the search input. 41 | * @param detail The details of the search input. 42 | * @param refresh Use the provided functions to update the table. 43 | */ 44 | export type SearchHandler = (result: RefreshDetail, detail: SearchDetail) => Promise | void; 45 | 46 | export type FilterDetailItem = { 47 | header: WithRequired; 48 | isDefault: boolean; 49 | }; 50 | export type FilterDetail = { 51 | current: FilterDetailItem; 52 | all: FilterDetailItem[]; 53 | }; 54 | /** 55 | * A function that gets called after the table filters changed. 56 | * @param detail The details of the filters. 57 | * @param refresh Use the provided functions to update the table. 58 | */ 59 | export type FilterHandler = (result: RefreshDetail, detail: FilterDetail) => Promise | void; 60 | 61 | export type SortDetail = { 62 | header: WithRequired; 63 | direction: SortDirection; 64 | }; 65 | export type InternalSortDetail = SortDetail & { 66 | id: string; 67 | }; 68 | /** 69 | * A function that gets called after the table sorting changed. 70 | * @param detail The details of the sorting. 71 | * @param refresh Use the provided functions to update the table. 72 | */ 73 | export type SortHandler = (result: RefreshDetail, detail: SortDetail) => Promise | void; 74 | 75 | export type SelectDetail = { item: TableItem; index: number }[]; 76 | export type SelectHandler = (detail: SelectDetail) => void; 77 | -------------------------------------------------------------------------------- /src/lib/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './filters.js'; 2 | export * from './handlers.js'; 3 | export * from './search.js'; 4 | export * from './sort.js'; 5 | export * from './table.js'; 6 | export * from './utils.js'; 7 | -------------------------------------------------------------------------------- /src/lib/types/refresh.ts: -------------------------------------------------------------------------------- 1 | export type LastRefreshDetail = { 2 | /** A date object representing the last refresh. */ 3 | refreshedAt: Date; 4 | /** 5 | * The auto-updating formatted relative time since the last refresh. 6 | */ 7 | value: string; 8 | /** Stops the ticking of the auto-updating value. */ 9 | cancel: () => void; 10 | }; 11 | 12 | export type RefreshLabelUpdateTickRate = 'second' | 'minute'; 13 | -------------------------------------------------------------------------------- /src/lib/types/search.ts: -------------------------------------------------------------------------------- 1 | import type { CustomFiltering } from './filters.js'; 2 | 3 | type TableSearchMode = { 4 | /** 5 | * If the mode of search is `server`, you'll have to handle it on the server side. 6 | * To do this, provide the `onSearch` function for the table. 7 | * 8 | * Setting this to anything other than `server` will make the search client sided. 9 | * 10 | * __WARNING:__ Having client side search with infinite loading enabled can trigger many `infinite` events. 11 | */ 12 | mode: T; 13 | }; 14 | 15 | export type TableSearchSettings = 16 | | TableSearchMode<'server'> 17 | | (TableSearchMode<'auto'> & { 18 | /** 19 | * The property to search by. Can be a dot-separated nested property, or an array of properties. 20 | * 21 | * The value that `property` points to must be of type `string`, `number`, or `boolean`. 22 | * @example 23 | * 'user.name' 24 | * // or 25 | * ['id', 'user.name'] 26 | * // where both 'id' and 'user.name' have string, number, or boolean values 27 | */ 28 | property: string | string[]; 29 | /** 30 | * If true, the filter will be case sensitive. 31 | * 32 | * @default false 33 | */ 34 | caseSensitive?: boolean; 35 | }) 36 | | (TableSearchMode<'custom'> & { 37 | /** 38 | * A custom filtering function that gets called on each item in the table. 39 | * @property item The item to filter. 40 | * @property index The index of the item in the table. 41 | * @returns `true` if the item should be shown, `false` if it should be filtered out. 42 | */ 43 | onSearch: CustomFiltering; 44 | }); 45 | -------------------------------------------------------------------------------- /src/lib/types/sort.ts: -------------------------------------------------------------------------------- 1 | export type SortDirection = 'asc' | 'desc'; 2 | 3 | type TableSortMode = { 4 | /** 5 | * If the mode of sorting is `auto`, the sorting will be done on the client automatically, on the existing items. 6 | * To do this, provide the `onSort` function for the table. 7 | * 8 | * If the mode of sorting is `server`, you'll have to handle it on the server side. 9 | * 10 | * Setting this to anything other than `server` will make the sort client sided. 11 | */ 12 | mode: T; 13 | }; 14 | 15 | export type TableHeaderSortProperty = string; 16 | export type TableHeaderServerSort = TableSortMode<'server'>; 17 | export type TableHeaderAutoSort = TableSortMode<'auto'> & { 18 | /** The property to sort by. Can be a dot-separated nested property (eg. `'user.name'`). */ 19 | property: TableHeaderSortProperty; 20 | }; 21 | export type TableHeaderSort = { 22 | defaultDirection?: SortDirection; 23 | } & (TableHeaderServerSort | TableHeaderAutoSort); 24 | -------------------------------------------------------------------------------- /src/lib/types/table.ts: -------------------------------------------------------------------------------- 1 | import type { TableHeaderFilter } from './filters.js'; 2 | import type { TableHeaderSort } from './sort.js'; 3 | import type { AnyRecord } from './utils.js'; 4 | 5 | export type TableItem = AnyRecord; 6 | 7 | export type TableBaseHeader = { 8 | label: string; 9 | filter?: TableHeaderFilter; 10 | sort?: TableHeaderSort; 11 | style?: TableHeaderStyle; 12 | /** 13 | * Additional data that can be used to add custom information about the header. 14 | */ 15 | meta?: M; 16 | }; 17 | 18 | export type TableHeaderStyle = { 19 | /** 20 | * If it's a number, it'll be treated as a pixel value. 21 | * If it's a string, it'll be treated as a CSS value. 22 | * Otherwise, defaults to 'auto'. 23 | */ 24 | width?: string | number; 25 | /** 26 | * `minWidth` must be a number, and will be treated as a pixel value. 27 | */ 28 | minWidth?: number; 29 | }; 30 | 31 | export type TableHeader = TableBaseHeader; 32 | -------------------------------------------------------------------------------- /src/lib/types/utils.ts: -------------------------------------------------------------------------------- 1 | export type WithRequired = T & { [P in K]-?: T[P] }; 2 | 3 | export type WithOptional = Omit & Partial>; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | export type AnyRecord = Record; 7 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | {@render children()} 9 |
10 | 11 | 12 | Svelte Infinitable - virtual table component • Adevien 13 | 14 | -------------------------------------------------------------------------------- /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |

Svelte Infinitable

9 |
10 | 18 |
19 |

20 | Svelte Infinitable is a virtual table component that uses native table elements and 21 | supports infinite scrolling, searching, filtering, 22 | sorting, and more. 23 |

24 |

25 | It integrates seamlessly into projects that use shadcn 29 | components, but it's flexible and customizable enough to fit the needs of any other project. 30 |

31 |
32 |
33 | 34 |
35 | -------------------------------------------------------------------------------- /src/routes/example/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data.js'; 2 | export * from './service.js'; 3 | export * from './table.js'; 4 | export { default as ExampleTable } from './table.svelte'; 5 | export * from './types.js'; 6 | export * from './utils.js'; 7 | -------------------------------------------------------------------------------- /src/routes/example/service.ts: -------------------------------------------------------------------------------- 1 | import type { SortDirection } from '$lib/types/index.js'; 2 | import { projects, tasks } from './data.js'; 3 | import type { ProjectData, TaskData } from './types.js'; 4 | 5 | export async function getTasks( 6 | page = 1, 7 | limit = 100, 8 | search = '', 9 | filter: Partial> = {}, 10 | sortBy: keyof TaskData = 'created_at', 11 | direction: SortDirection = 'desc' 12 | ): Promise<{ data: TaskData[]; depleted: boolean; total: number }> { 13 | return new Promise((resolve) => { 14 | setTimeout(() => { 15 | let filteredTasks = [...tasks]; 16 | 17 | if (search) { 18 | search = search.trim().toLowerCase(); 19 | filteredTasks = filteredTasks.filter((task) => { 20 | return task.name.trim().toLowerCase().includes(search); 21 | }); 22 | } 23 | 24 | if (Object.keys(filter).length) { 25 | filteredTasks = filteredTasks.filter((task) => { 26 | return Object.entries(filter).every(([key, values]) => { 27 | const value = task[key as keyof TaskData].toLowerCase(); 28 | return values.some((v) => value.includes(v.toLowerCase())); 29 | }); 30 | }); 31 | } 32 | 33 | if (sortBy && direction) { 34 | filteredTasks.sort((a, b) => { 35 | const aValue = a[sortBy]; 36 | const bValue = b[sortBy]; 37 | const comparison = aValue.localeCompare(bValue); 38 | if (comparison) { 39 | return direction === 'asc' ? comparison : -comparison; 40 | } 41 | return 0; 42 | }); 43 | } 44 | 45 | const data = filteredTasks.slice((page - 1) * limit, page * limit) as TaskData[]; 46 | resolve({ 47 | data, 48 | depleted: page * limit >= filteredTasks.length, 49 | total: tasks.length 50 | }); 51 | }, 1000); 52 | }); 53 | } 54 | 55 | export async function getProjects(): Promise { 56 | return new Promise((resolve) => { 57 | setTimeout(() => { 58 | resolve(projects); 59 | }, 1000); 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /src/routes/example/table.svelte: -------------------------------------------------------------------------------- 1 | 121 | 122 | isFinishedTaskState(state)} 128 | disabledRowMessage="Task is already finished" 129 | {onInfinite} 130 | onFilter={searchFilterSortHandler} 131 | onSort={searchFilterSortHandler} 132 | {onSelect} 133 | class="h-[60vh] min-h-[400px]" 134 | debug 135 | > 136 | {#snippet actions()} 137 | 138 |
139 | 144 |
145 | 149 | 150 | 151 |
152 | {/snippet} 153 | 154 | {#snippet headers()} 155 | {#each tableHeaders as header} 156 | 157 | {/each} 158 | {/snippet} 159 | 160 | {#snippet children({ index })} 161 | {@const { id, name, project_name, state, created_at } = items[index] ?? {}} 162 | {@const label = name || id} 163 | 164 | {label} 165 | 166 | 167 | {project_name} 168 | 169 | 170 | 171 | 175 | {taskStateData[state]?.label ?? 'Unkown'} 176 | 177 | 178 | 179 | {formatDateString(created_at)} 180 | 181 | 182 | 183 | 190 | Row menu 191 | 192 | 193 | 194 | alert('Mock action')}> 195 | 196 | Details 197 | 198 | alert('Mock action')}> 199 | 200 | Cancel task 201 | 202 | 203 | 204 | 205 | {/snippet} 206 | 207 | {#snippet completedEmpty()} 208 | 209 |

No tasks found

210 | {/snippet} 211 | 212 | {#snippet rowsDetail({ rowCount, selectedCount })} 213 |

214 | {rowCount} of {totalCount} task{rowCount === 1 ? '' : 's'} shown, {selectedCount} selected 215 |

216 | {/snippet} 217 |
218 | -------------------------------------------------------------------------------- /src/routes/example/table.ts: -------------------------------------------------------------------------------- 1 | import type { TableHeader } from '$lib/types/index.js'; 2 | import type { TaskData, TaskState } from './types.js'; 3 | 4 | export const taskStateData = { 5 | 'timed-out': { 6 | label: 'Timed out', 7 | color: 'bg-orange-600' 8 | }, 9 | failed: { 10 | label: 'Failed', 11 | color: 'bg-red-700' 12 | }, 13 | completed: { 14 | label: 'Completed', 15 | color: 'bg-green-700' 16 | }, 17 | cancelled: { 18 | label: 'Canceled', 19 | color: 'bg-yellow-500' 20 | }, 21 | queued: { 22 | label: 'Queued', 23 | color: 'bg-indigo-700' 24 | }, 25 | running: { 26 | label: 'Running', 27 | color: 'bg-blue-500' 28 | } 29 | } as const satisfies Record; 30 | 31 | export const statusOptions: { name: TaskState; label: string }[] = [ 32 | { 33 | name: 'failed', 34 | label: taskStateData.failed.label 35 | }, 36 | { 37 | name: 'timed-out', 38 | label: taskStateData['timed-out'].label 39 | }, 40 | { 41 | name: 'queued', 42 | label: taskStateData.queued.label 43 | }, 44 | { 45 | name: 'running', 46 | label: taskStateData.running.label 47 | }, 48 | { 49 | name: 'cancelled', 50 | label: taskStateData['cancelled'].label 51 | }, 52 | { 53 | name: 'completed', 54 | label: taskStateData['completed'].label 55 | } 56 | ]; 57 | 58 | export const tableHeaders: TableHeader<{ name: keyof TaskData }>[] = [ 59 | { 60 | label: 'Task', 61 | meta: { 62 | name: 'name' 63 | }, 64 | sort: { 65 | mode: 'server' 66 | }, 67 | style: { 68 | minWidth: 250 69 | } 70 | }, 71 | { 72 | label: 'Project', 73 | meta: { 74 | name: 'project_name' 75 | }, 76 | sort: { 77 | mode: 'server' 78 | }, 79 | filter: { 80 | type: 'text', 81 | mode: 'server', 82 | placeholder: 'Filter by project name or ID' 83 | }, 84 | style: { 85 | minWidth: 250 86 | } 87 | }, 88 | { 89 | label: 'Status', 90 | meta: { 91 | name: 'state' 92 | }, 93 | sort: { 94 | mode: 'server' 95 | }, 96 | filter: { 97 | type: 'multiSelect', 98 | mode: 'server', 99 | options: statusOptions, 100 | value: statusOptions 101 | }, 102 | style: { 103 | width: 180 104 | } 105 | }, 106 | { 107 | label: 'Created', 108 | meta: { 109 | name: 'created_at' 110 | }, 111 | sort: { 112 | mode: 'server', 113 | defaultDirection: 'asc' 114 | }, 115 | style: { 116 | width: 150 117 | } 118 | }, 119 | { 120 | label: '', 121 | style: { 122 | width: 40 123 | } 124 | } 125 | ]; 126 | -------------------------------------------------------------------------------- /src/routes/example/types.ts: -------------------------------------------------------------------------------- 1 | export type TaskState = 'timed-out' | 'failed' | 'completed' | 'queued' | 'running' | 'cancelled'; 2 | 3 | export type TaskClientAction = 'validation' | 'export' | 'upload'; 4 | 5 | export type ProjectData = { 6 | id: string; 7 | name: string; 8 | }; 9 | 10 | export type TaskData = { 11 | id: string; 12 | name: string; 13 | project_id: string; 14 | project_name: string; 15 | created_at: string; 16 | state: TaskState; 17 | }; 18 | -------------------------------------------------------------------------------- /src/routes/example/utils.ts: -------------------------------------------------------------------------------- 1 | import type { TaskState } from './types.js'; 2 | 3 | export function formatDateString(dateString: string) { 4 | return new Date(dateString).toLocaleString(); 5 | } 6 | 7 | export function isFinishedTaskState(state: TaskState) { 8 | if (!state) { 9 | return true; 10 | } 11 | const finishedStates: TaskState[] = ['completed', 'failed', 'timed-out', 'cancelled']; 12 | return finishedStates.includes(state); 13 | } 14 | -------------------------------------------------------------------------------- /src/routes/icons/github.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 20 | 21 | -------------------------------------------------------------------------------- /src/routes/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as GitHub } from './github.svelte'; 2 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adevien-solutions/svelte-infinitable/705384619423a4492697395f7759549a03f52a6e/static/favicon.png -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-cloudflare'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | import { mdsvex } from 'mdsvex'; 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | // Consult https://svelte.dev/docs/kit/integrations 8 | // for more information about preprocessors 9 | preprocess: [vitePreprocess(), mdsvex()], 10 | 11 | kit: { 12 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 13 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 14 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 15 | adapter: adapter(), 16 | alias: { 17 | '@/*': './src/lib/components/*' 18 | } 19 | }, 20 | 21 | extensions: ['.svelte', '.svx'] 22 | }; 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import typography from '@tailwindcss/typography'; 2 | import type { Config } from 'tailwindcss'; 3 | import tailwindcssAnimate from 'tailwindcss-animate'; 4 | import { fontFamily } from 'tailwindcss/defaultTheme'; 5 | 6 | const config: Config = { 7 | darkMode: ['class'], 8 | content: ['./src/**/*.{html,js,svelte,ts}'], 9 | safelist: ['dark'], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: '2rem', 14 | screens: { 15 | '2xl': '1400px' 16 | } 17 | }, 18 | extend: { 19 | colors: { 20 | border: 'hsl(var(--border) / )', 21 | input: 'hsl(var(--input) / )', 22 | ring: 'hsl(var(--ring) / )', 23 | background: 'hsl(var(--background) / )', 24 | foreground: 'hsl(var(--foreground) / )', 25 | primary: { 26 | DEFAULT: 'hsl(var(--primary) / )', 27 | foreground: 'hsl(var(--primary-foreground) / )' 28 | }, 29 | secondary: { 30 | DEFAULT: 'hsl(var(--secondary) / )', 31 | foreground: 'hsl(var(--secondary-foreground) / )' 32 | }, 33 | destructive: { 34 | DEFAULT: 'hsl(var(--destructive) / )', 35 | foreground: 'hsl(var(--destructive-foreground) / )' 36 | }, 37 | muted: { 38 | DEFAULT: 'hsl(var(--muted) / )', 39 | foreground: 'hsl(var(--muted-foreground) / )' 40 | }, 41 | accent: { 42 | DEFAULT: 'hsl(var(--accent) / )', 43 | foreground: 'hsl(var(--accent-foreground) / )' 44 | }, 45 | popover: { 46 | DEFAULT: 'hsl(var(--popover) / )', 47 | foreground: 'hsl(var(--popover-foreground) / )' 48 | }, 49 | card: { 50 | DEFAULT: 'hsl(var(--card) / )', 51 | foreground: 'hsl(var(--card-foreground) / )' 52 | }, 53 | sidebar: { 54 | DEFAULT: 'hsl(var(--sidebar-background))', 55 | foreground: 'hsl(var(--sidebar-foreground))', 56 | primary: 'hsl(var(--sidebar-primary))', 57 | 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', 58 | accent: 'hsl(var(--sidebar-accent))', 59 | 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', 60 | border: 'hsl(var(--sidebar-border))', 61 | ring: 'hsl(var(--sidebar-ring))' 62 | } 63 | }, 64 | borderRadius: { 65 | xl: 'calc(var(--radius) + 4px)', 66 | lg: 'var(--radius)', 67 | md: 'calc(var(--radius) - 2px)', 68 | sm: 'calc(var(--radius) - 4px)' 69 | }, 70 | fontFamily: { 71 | sans: [...fontFamily.sans] 72 | }, 73 | keyframes: { 74 | 'accordion-down': { 75 | from: { height: '0' }, 76 | to: { height: 'var(--bits-accordion-content-height)' } 77 | }, 78 | 'accordion-up': { 79 | from: { height: 'var(--bits-accordion-content-height)' }, 80 | to: { height: '0' } 81 | }, 82 | 'caret-blink': { 83 | '0%,70%,100%': { opacity: '1' }, 84 | '20%,50%': { opacity: '0' } 85 | } 86 | }, 87 | animation: { 88 | 'accordion-down': 'accordion-down 0.2s ease-out', 89 | 'accordion-up': 'accordion-up 0.2s ease-out', 90 | 'caret-blink': 'caret-blink 1.25s ease-out infinite' 91 | } 92 | } 93 | }, 94 | plugins: [tailwindcssAnimate, typography] 95 | }; 96 | 97 | export default config; 98 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "module": "NodeNext", 13 | "moduleResolution": "NodeNext" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | 7 | test: { 8 | include: ['src/**/*.{test,spec}.{js,ts}'] 9 | } 10 | }); 11 | --------------------------------------------------------------------------------