Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
28cba8a
feat: added new component called expandable table aswell as left chev…
HarshMN2345 Aug 15, 2025
6325730
fix:spacing issue
HarshMN2345 Aug 16, 2025
6f0c94b
fix: responsiveness
HarshMN2345 Aug 16, 2025
4f3c872
fix: remove left chevron variants of accordian
HarshMN2345 Aug 17, 2025
18909bc
feat: add slot-based API (Root/Row/Cell)
HarshMN2345 Aug 18, 2025
2a06487
Merge branch 'next' into feat-SER-205-new-expansion-table-component
HarshMN2345 Aug 21, 2025
0f02493
fix:improve type structure
HarshMN2345 Aug 24, 2025
f2c492e
fix:lint error
HarshMN2345 Aug 24, 2025
60a0d5c
standardize 40px row height and fix alignment and used same icon comp…
HarshMN2345 Aug 25, 2025
2cf27e0
Merge branch 'feat-spreadsheet' into feat-SER-205-new-expansion-table…
HarshMN2345 Aug 28, 2025
79d8d17
fix: deleted types.ts and moved rootprops to index.ts
HarshMN2345 Aug 29, 2025
7a84c31
passing isOpen and toggle to handled by row
HarshMN2345 Aug 29, 2025
9659e5f
Merge branch 'feat-spreadsheet' into feat-SER-205-new-expansion-table…
HarshMN2345 Aug 31, 2025
40fb564
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 2, 2025
6ef18ae
feat: accordion table.
ItzNotABug Sep 2, 2025
101733c
address comments.
ItzNotABug Sep 8, 2025
86cdb73
Merge branch 'next' into accordion-table
ItzNotABug Sep 8, 2025
8a3316e
Merge branch 'next' into accordion-table
ItzNotABug Sep 8, 2025
3d9d31f
Merge branch 'next' into accordion-table
ItzNotABug Sep 8, 2025
317dd3f
Merge branch 'next' into accordion-table
ItzNotABug Sep 10, 2025
40105cc
update: handle viewports on accordion table summary.
ItzNotABug Sep 10, 2025
aceaca7
Merge branch 'next' into accordion-table
ItzNotABug Sep 11, 2025
034243e
Merge branch 'next' into accordion-table
ItzNotABug Sep 11, 2025
0d30abc
Merge branch 'next' into accordion-table
ItzNotABug Sep 11, 2025
dd980f7
Update v2/pink-sb/src/lib/accordion-table/cell/Cell.svelte
ItzNotABug Sep 11, 2025
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
45 changes: 45 additions & 0 deletions v2/pink-sb/src/lib/accordion-table/cell/Cell.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script lang="ts">
import Icon from '$lib/Icon.svelte';
import type { RootProp } from '../index.js';
import Stack from '$lib/layout/Stack.svelte';
import { type ComponentProps, getContext } from 'svelte';
import { IconChevronDown } from '@appwrite.io/pink-icons-svelte';

export let root: RootProp;
export let column: string;

$: expandable = getContext<boolean>('expandable');

let justify: ComponentProps<Stack>['justifyContent'];

$: isFirstColumn = root.isFirstColumn(column);
$: columnAlignment = root.getColumn(column)?.align ?? 'left';

$: justify =
columnAlignment === 'right'
? 'flex-end'
: columnAlignment === 'center'
? 'center'
: 'flex-start';
</script>

<Stack direction="row" alignItems="center" gap="xxs" justifyContent={justify}>
<slot />
</Stack>

{#if !expandable && isFirstColumn}
<!-- needed to balance out the empty spacing when not expandable -->
<span class="balancing-placeholder">
<!-- placeholder icon to fit dimensions -->
<Icon icon={IconChevronDown} size="s"></Icon>
</span>
{/if}

<style>
.balancing-placeholder {
margin-inline: 2px;
visibility: hidden;
touch-action: none;
pointer-events: none;
}
</style>
49 changes: 49 additions & 0 deletions v2/pink-sb/src/lib/accordion-table/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Root from './root/Root.svelte';
import Row from './row/Row.svelte';
import Cell from './cell/Cell.svelte';
import SummaryRow from '../table/row/Base.svelte';
import SummaryCell from '../table/cell/Base.svelte';

export type Align = 'left' | 'center' | 'right';

export type Column = {
id: string;
align?: Align;
width?:
| {
min: number;
max: number;
}
| {
min: number;
}
| number;
hide?: boolean;
};

export type RootProp = {
// Open state controls
single: boolean;
openIds: string[];
isOpen: (id: string) => boolean;
open: (id: string) => void;
close: (id: string) => void;
toggle: (id: string) => void;
register: (id: string) => void;
unregister: (id: string) => void;
isFirstColumn: (id: string) => boolean;
getColumn: (id: string) => Column | undefined;

columns: Array<Column>;
columnsCount: number;
};

export default {
Root,
Row,
Cell,
Summary: {
Row: SummaryRow,
Cell: SummaryCell
}
};
124 changes: 124 additions & 0 deletions v2/pink-sb/src/lib/accordion-table/root/Root.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<script lang="ts">
import type { Column, RootProp } from '../index.js';

export let openIds: string[] = [];
export let single: boolean = false;
export let columns: Array<Column> | number;

let registeredIds: Set<string> = new Set();

function isOpen(id: string): boolean {
return openIds.includes(id);
}

function open(id: string): void {
if (!registeredIds.has(id)) return;
if (single) {
openIds = [id];
} else if (!openIds.includes(id)) {
openIds = [...openIds, id];
}
}

function close(id: string): void {
if (!registeredIds.has(id)) return;
openIds = openIds.filter((openId) => openId !== id);
}

function toggle(id: string): void {
if (isOpen(id)) close(id);
else open(id);
}

function register(id: string): void {
registeredIds.add(id);
registeredIds = registeredIds;
}

function unregister(id: string): void {
registeredIds.delete(id);
registeredIds = registeredIds;
openIds = openIds.filter((openId) => openId !== id);
}

function getColumn(columnId: string): Column | undefined {
return columnsArray.find((column) => column.id === columnId);
}

function isFirstColumn(columnId: string): boolean {
return columnsArray.length > 0 && columnsArray[0].id === columnId;
}

$: columnsArray =
typeof columns === 'number'
? Array.from({ length: Math.max(1, Math.floor(columns)) }, (_, i) => ({
id: i.toString(),
align: 'left' as const
}))
: columns;

$: columnsCount = columnsArray.length;

$: root = {
single,
openIds,
isOpen,
open,
close,
toggle,
register,
unregister,
getColumn,
isFirstColumn,

columns: columnsArray,
columnsCount
} as RootProp;
</script>

<div class="accordion-table">
<div class="body">
<slot {root} />
</div>
</div>

<style lang="scss">
.accordion-table {
--row-gap: 4px;
--row-height: 40px;

--divider-color: var(--border-neutral, rgba(0, 0, 0, 0.12));
--divider-strong: var(--border-neutral-strong, rgba(0, 0, 0, 0.18));
--overlay-hover: var(--overlay-neutral-hover, rgba(0, 0, 0, 0.04));
--row-open-bg: var(--bgcolor-neutral-default, rgba(0, 0, 0, 0.02));
--accordion-bg: var(--bgcolor-neutral-default, var(--row-open-bg));

border: var(--border-width-s, 1px) solid var(--divider-strong);
border-radius: var(--border-radius-s);
background: var(--bgcolor-neutral-primary);
overflow-x: auto;
overflow-y: hidden;
width: 100%;
max-width: 100%;
-webkit-overflow-scrolling: touch;
}

@media (prefers-color-scheme: dark) {
.accordion-table {
--divider-color: var(--border-neutral, rgba(255, 255, 255, 0.08));
--divider-strong: var(--border-neutral-strong, rgba(255, 255, 255, 0.12));
--overlay-hover: var(--overlay-neutral-hover, rgba(255, 255, 255, 0.02));
--row-open-bg: var(--bgcolor-neutral-default-dark, rgba(255, 255, 255, 0.02));
}
}

.body {
width: 100%;
display: block;

// remove bottom border of the last row
& :global(.table-row:last-of-type .row-content) {
border-bottom: unset !important;
}
}
</style>
137 changes: 137 additions & 0 deletions v2/pink-sb/src/lib/accordion-table/row/Row.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<script lang="ts">
import { onMount, setContext } from 'svelte';

import Icon from '$lib/Icon.svelte';
import { slide } from 'svelte/transition';
import type { RootProp } from '../index.js';
import { createEventDispatcher } from 'svelte';
import Button from '../../button/Button.svelte';
import { IconChevronDown } from '@appwrite.io/pink-icons-svelte';
import Summary from '$lib/accordion-table/summary/Summary.svelte';

export let id: string;
export let root: RootProp;
export let expandable: boolean = true;
export let defaultOpen: boolean = false;

const dispatch = createEventDispatcher<{
toggle: { id: string; open: boolean };
open: { id: string };
close: { id: string };
}>();

onMount(() => {
if (id) root.register(id);
if (defaultOpen && id) root.open(id);

return () => {
if (id) root.unregister(id);
};
});

$: isOpen = root.isOpen(id);
$: rowGridTemplate = `repeat(${root.columnsCount}, 1fr)`;

setContext('expandable', expandable);

function toggleRow() {
if (!expandable) return;

const wasOpen = isOpen;
root.toggle(id);

const nowOpen = !wasOpen;
dispatch('toggle', { id, open: nowOpen });

if (nowOpen) dispatch('open', { id });
else dispatch('close', { id });
}
</script>

<div class="table-row" class:has-children={expandable} class:is-open={isOpen}>
{#if expandable}
<div class="row-content">
<Button variant="compact" on:click={toggleRow} style="width: 100%">
<span class="chevron" class:is-open={isOpen} style:display="flex">
<Icon icon={IconChevronDown} size="s"></Icon>
</span>

<slot {id} />
</Button>
</div>
{:else}
<div class="row-content" style:grid-template-columns={rowGridTemplate}>
<slot {id} />
</div>
{/if}

{#if isOpen}
<div class="expanded-content" transition:slide={{ duration: 200 }}>
<Summary {root} let:table>
<slot name="summary" {id} root={table} />
</Summary>
</div>
{/if}
</div>

<style lang="scss">
.table-row {
&.has-children:not(.is-open):hover .row-content {
background: var(--overlay-hover);

.chevron {
color: var(--fgcolor-neutral-secondary);
}
}

&.is-open .row-content {
background: var(--bgcolor-neutral-primary);
border-bottom-color: var(--divider-strong);
}
}

.row-content {
display: flex;
align-items: center !important;
width: 100% !important;
height: var(--row-height);
padding: 4px 12px !important;
--p-button-border-radius: 0;

border-bottom: var(--border-width-s, 1px) solid var(--divider-color) !important;

& > :global(*) {
height: 100%;
}
}

.chevron {
color: var(--fgcolor-neutral-tertiary);
transition:
rotate 300ms ease-in-out,
color 200ms ease-in-out;

&.is-open {
rotate: 180deg;
color: var(--fgcolor-neutral-secondary);
}
}

.expanded-content {
padding: 0;
position: relative;
background: var(--bgcolor-neutral-default);
}

.expanded-content::after {
left: 0;
right: 0;
bottom: 0;
z-index: 1;
content: '';
position: absolute;
pointer-events: none;
height: var(--border-width-s);
background: var(--divider-strong);
}
</style>
Loading
Loading