Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions src/lib/ui/core/Table/DataTable/DataTable.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<script
lang="ts"
generics="GItem extends Record<string, unknown>, GColumn extends BaseTableColumn<GItem>"
>
import type { ComponentProps } from 'svelte'
import type { BaseTableColumn } from './types.js'

import Table from '../Table.svelte'
import TableBody from '../TableBody.svelte'
import TableCell from '../TableCell.svelte'
import TableHead from '../TableHead.svelte'
import TableHeader from '../TableHeader.svelte'
import TableRow from '../TableRow.svelte'
import Pagination from '../Pagination.svelte'

type TProps = Partial<ComponentProps<typeof Pagination>> & {
items: GItem[]
columns: GColumn[]

class?: string
headerClass?: string
bodyClass?: string
headerRowClass?: string
bodyRowClass?: string

paged?: boolean
}

const {
items,
columns,
class: className,
headerClass,
bodyClass,
headerRowClass,
bodyRowClass,
totalItems,
page = 1,
pageSize = items.length,
rows,
onPageChange,
paged,
}: TProps = $props()

const hasMoreItems = $derived(items.length !== totalItems)
const itemsCount = $derived(totalItems ?? items.length)
const pageOffset = $derived((page - 1) * pageSize)
const pageEndOffset = $derived(pageOffset + pageSize)

const pagedItems = $derived.by(() => {
console.log({ paged, hasMoreItems, pageOffset, pageEndOffset })
if (!paged) return items
if (hasMoreItems) return items.slice(0, pageSize)

return items.slice(pageOffset, pageEndOffset)
})
</script>

<article class="flex w-full flex-col gap-4">
<Table class={className}>
<TableHeader class={headerClass}>
<TableRow class={headerRowClass}>
{#each columns as { title, Head, class: className }}
{#if Head}
<Head />
{:else}
<TableHead class={className}>{title}</TableHead>
{/if}
{/each}
</TableRow>
</TableHeader>
<TableBody class={bodyClass}>
{#each pagedItems as item, i}
<TableRow class={bodyRowClass}>
{#each columns as column}
{#if column.Cell}
<column.Cell {item} />
{:else}
<TableCell class={column.class}>
{column.format(item, i)}
</TableCell>
{/if}
{/each}
</TableRow>
{/each}
</TableBody>
</Table>

{#if paged}
<Pagination {page} {pageSize} {rows} {onPageChange} totalItems={itemsCount} />
{/if}
</article>
25 changes: 25 additions & 0 deletions src/lib/ui/core/Table/DataTable/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Component } from 'svelte'

// TODO: Maybe it's not worth it to use union for format/Cell and title/Head options
export type BaseTableColumn<GItem extends Record<string, unknown>> = {
class?: string
} & (
| {
Cell: Component<{ item: GItem }>
format?: undefined
}
| {
Cell?: undefined
format: (item: GItem, index: number) => string | number
}
) &
(
| {
title: string
Head?: undefined
}
| {
title?: undefined
Head: Component
}
)
147 changes: 147 additions & 0 deletions src/lib/ui/core/Table/Pagination.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<script lang="ts">
import type { ChangeEventHandler } from 'svelte/elements'

import { noop } from '@melt-ui/svelte/internal/helpers'

import Button from '$ui/core/Button/Button.svelte'
import Input from '$ui/core/Input/Input.svelte'
import Popover from '$ui/core/Popover/Popover.svelte'
import { cn } from '$ui/utils/index.js'

type TProps = {
class?: string
page?: number
pageSize?: number
rows?: number[]
onPageChange?: (page: number, pageSize: number) => void
totalItems: number
}

const {
class: className,
page = 1,
pageSize = 25,
rows = [10, 25, 50],
onPageChange = noop,
totalItems,
}: TProps = $props()

let isPageSizeOpened = $state(false)

$effect(() => {
if (!process.env.IS_DEV_MODE) return

if (rows.length > 0 && !rows.includes(pageSize)) {
console.error('[pageSize] value should be present in [rows] array or it should be empty')
}
})

const pagesAmount = $derived(Math.ceil(totalItems / pageSize))
const maxPage = $derived(pagesAmount)

function adjustPageNumber(page: number) {
if (!page || page < 1) return 1
if (page > pagesAmount) return maxPage
return page
}

const onPageInput: ChangeEventHandler<HTMLInputElement> = ({ currentTarget }) => {
const value = +currentTarget.value
const page = adjustPageNumber(value)

onPageChange(page, pageSize)
}

function onPageSizeChange(size: number) {
isPageSizeOpened = false
onPageChange(1, size)
}

function onNextPage() {
onPageChange(adjustPageNumber(page + 1), pageSize)
}

function onPrevPage() {
onPageChange(adjustPageNumber(page - 1), pageSize)
}
</script>

<section class={cn('flex items-center gap-4 whitespace-nowrap', className)}>
{#if rows.length > 1}
<Popover bind:isOpened={isPageSizeOpened}>
{#snippet children({ props })}
<Button
{...props}
class="sm:hidden"
variant="border"
size="md"
icon="arrow-down"
iconSize="8"
iconHeight="5"
iconOnRight
>
{pageSize} rows
</Button>
{/snippet}

{#snippet content()}
<section class="flex flex-col">
{#each rows as option}
<Button
class={cn(option === pageSize && 'text-green')}
onclick={() => onPageSizeChange(option)}
>
{option} rows
</Button>
{/each}
</section>
{/snippet}
</Popover>
{/if}

<section class="flex items-center gap-2 text-waterloo">
<span>Page</span>

<Input
class="text-center"
inputClass="[appearance:textfield]"
type="number"
min="1"
max={maxPage}
value={page}
onchange={onPageInput}
/>

<section>
<span>of {pagesAmount}</span>
</section>
</section>

<section class="ml-auto flex gap-2">
<Button
class="[&>svg]:rotate-180"
variant="border"
disabled={page <= 1}
size="md"
icon="arrow-right"
iconSize="5"
iconHeight="8"
iconOnRight
onclick={onPrevPage}
>
Prev
</Button>

<Button
variant="border"
disabled={page >= maxPage}
size="md"
icon="arrow-right"
iconSize="5"
iconHeight="8"
onclick={onNextPage}
>
Next
</Button>
</section>
</section>
17 changes: 17 additions & 0 deletions src/lib/ui/core/Table/Table.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script lang="ts">
import type { Snippet } from 'svelte'
import type { HTMLTableAttributes } from 'svelte/elements'

import { cn } from '$ui/utils/index.js'

type TProps = {
class?: string
children: Snippet
} & HTMLTableAttributes

const { class: className, children, ...props }: TProps = $props()
</script>

<table class={cn('relative w-full border-spacing-0 text-left', className)} {...props}>
{@render children()}
</table>
17 changes: 17 additions & 0 deletions src/lib/ui/core/Table/TableBody.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script lang="ts">
import type { Snippet } from 'svelte'
import type { HTMLAttributes } from 'svelte/elements'

import { cn } from '$ui/utils/index.js'

type TProps = {
class?: string
children: Snippet
} & HTMLAttributes<HTMLTableSectionElement>

const { class: className, children, ...props }: TProps = $props()
</script>

<tbody class={cn('', className)} {...props}>
{@render children()}
</tbody>
23 changes: 23 additions & 0 deletions src/lib/ui/core/Table/TableCell.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script lang="ts">
import type { Snippet } from 'svelte'
import type { HTMLTdAttributes } from 'svelte/elements'

import { cn } from '$ui/utils/index.js'

type TProps = {
class?: string
children: Snippet
} & HTMLTdAttributes

const { class: className, children, ...props }: TProps = $props()
</script>

<td
class={cn(
'overflow-hidden text-ellipsis whitespace-nowrap px-2 group-hover:bg-athens',
className,
)}
{...props}
>
{@render children()}
</td>
23 changes: 23 additions & 0 deletions src/lib/ui/core/Table/TableHead.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script lang="ts">
import type { Snippet } from 'svelte'
import type { HTMLThAttributes } from 'svelte/elements'

import { cn } from '$ui/utils/index.js'

type TProps = {
class?: string
children: Snippet
} & HTMLThAttributes

const { class: className, children, ...props }: TProps = $props()
</script>

<th
class={cn(
'whitespace-nowrap border-b bg-athens px-[15px] py-[5px] font-medium text-casper',
className,
)}
{...props}
>
{@render children()}
</th>
18 changes: 18 additions & 0 deletions src/lib/ui/core/Table/TableHeader.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script lang="ts">
import type { Snippet } from 'svelte'
import type { HTMLAttributes } from 'svelte/elements'

import { cn } from '$ui/utils/index.js'

type TProps = {
class?: string
sticky?: boolean
children: Snippet
} & HTMLAttributes<HTMLTableSectionElement>

const { class: className, children, sticky = false, ...props }: TProps = $props()
</script>

<thead class={cn('', sticky && 'sticky top-0', className)} {...props}>
{@render children()}
</thead>
17 changes: 17 additions & 0 deletions src/lib/ui/core/Table/TableRow.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script lang="ts">
import type { Snippet } from 'svelte'
import type { HTMLAttributes } from 'svelte/elements'

import { cn } from '$ui/utils/index.js'

type TProps = {
class?: string
children: Snippet
} & HTMLAttributes<HTMLTableRowElement>

const { class: className, children, ...props }: TProps = $props()
</script>

<tr class={cn('group', className)} {...props}>
{@render children()}
</tr>
Loading