Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
abc3783
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 8, 2025
a02594a
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 8, 2025
780505d
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 8, 2025
6719952
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 8, 2025
062d3ec
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 8, 2025
fb8b1ed
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 8, 2025
c088e45
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 8, 2025
1489b16
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 10, 2025
d8326d0
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 11, 2025
e1c4a38
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 11, 2025
0fefd92
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 11, 2025
952b37e
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 12, 2025
fafc942
fix: dual sided resizing.
ItzNotABug Sep 13, 2025
aca40bd
Merge pull request #364 from appwrite/attempt-resize-fixes
ItzNotABug Sep 15, 2025
6fd5ef1
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 15, 2025
65e03a4
fix: dual-sided resizing and cache issue causing wrong resize.
ItzNotABug Sep 15, 2025
4e4e31d
lint.
ItzNotABug Sep 15, 2025
ba96092
update: misc.
ItzNotABug Sep 15, 2025
28b9e27
Merge pull request #365 from appwrite/fix-width-resize-and-swap
ItzNotABug Sep 15, 2025
263a9e2
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 15, 2025
2637bd0
update: misc.
ItzNotABug Sep 15, 2025
737010f
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 15, 2025
00be49b
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 15, 2025
5b11372
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 15, 2025
f4da718
fix: changes from next.
ItzNotABug Sep 15, 2025
66e8127
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 18, 2025
4a33959
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 18, 2025
f2198f1
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 20, 2025
4e5cc18
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 24, 2025
077179c
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 24, 2025
8f82877
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Sep 26, 2025
754a996
Merge branch 'next' into feat-SER-205-new-expansion-table-component
ItzNotABug Oct 3, 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
54 changes: 54 additions & 0 deletions v2/pink-sb/src/lib/expandable-table/cell/Cell.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<script lang="ts">
import type { RootProp } from '../index.js';
import Icon from '$lib/Icon.svelte';
import { IconChevronDown } from '@appwrite.io/pink-icons-svelte';
import Button from '$lib/button/Button.svelte';
import { getContext } from 'svelte';

export let root: RootProp;
export let column: string;
export let expandable: boolean = true;
const rowId = getContext<string>('rowId');

$: columnIndex = root.columns.findIndex((c) => c.id === column);
$: justify = root.alignment(root.columns[columnIndex]?.align);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need the root.alignment method. see - v2/pink-sb/src/lib/table/cell/Base.svelte

$: isFirstCell = columnIndex === 0;
$: isOpen = rowId ? root.isOpen(rowId) : false;
$: toggle = rowId ? () => root.toggle(rowId) : () => {};
</script>

<div class="cell" style="justify-content: {justify};">
{#if isFirstCell && expandable}
<Button
variant="ghost"
icon
size="s"
on:click={toggle}
aria-expanded={isOpen}
aria-label={isOpen ? 'Collapse row' : 'Expand row'}
>
<span class="chevron" class:open={isOpen}>
<Icon icon={IconChevronDown} size="s" />
</span>
</Button>
{/if}
<slot />
</div>

<style lang="scss">
.cell {
display: flex;
align-items: center;
gap: var(--space-2, 4px);
height: 100%;
}

.chevron {
display: flex;
transition: rotate 300ms ease-in-out;
}

.chevron.open {
rotate: 180deg;
}
</style>
31 changes: 31 additions & 0 deletions v2/pink-sb/src/lib/expandable-table/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Root from './root/Root.svelte';
import Row from './row/Row.svelte';
import Cell from './cell/Cell.svelte';

export type ExpandableTableColumn = {
id: string;
title?: string;
width?: string; // e.g. '2fr'
align?: 'left' | 'center' | 'right';
};

export type RootProp = {
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;
columns: ExpandableTableColumn[];
gridTemplateColumns: string;
childGridTemplate: string;
alignment: (align?: 'left' | 'center' | 'right') => string;
};

export default {
Root,
Row,
Cell
};
163 changes: 163 additions & 0 deletions v2/pink-sb/src/lib/expandable-table/root/Root.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<script lang="ts">
import type { ExpandableTableColumn } from '../index.js';
import Text from '$lib/typography/Text.svelte';

export let single: boolean = false;
export let openIds: string[] = [];
export let columns: ExpandableTableColumn[] = [];
export let showHeader: boolean = true;

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);
}

// Grid column templates
$: baseColumnWidths = columns.length ? columns.map((c) => c.width ?? '1fr').join(' ') : '1fr';
$: gridTemplateColumns = baseColumnWidths;
$: childGridTemplate = baseColumnWidths;

const alignment = (align?: 'left' | 'center' | 'right') => {
switch (align) {
case 'right':
return 'flex-end';
case 'center':
return 'center';
default:
return 'flex-start';
}
};

$: root = {
single,
openIds,
isOpen,
open,
close,
toggle,
register,
unregister,
columns,
gridTemplateColumns,
childGridTemplate,
alignment
};
</script>

<div class="expandable-table">
{#if showHeader && columns.length}
<div class="table-header" style="grid-template-columns: {gridTemplateColumns};">
<slot name="header">
{#each columns as col}
<div class="header-cell" style="justify-content: {alignment(col.align)};">
<Text variant="m-500" color="--fgcolor-neutral-secondary">{col.title}</Text>
</div>
{/each}
</slot>
</div>
{/if}

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

<style lang="scss">
.expandable-table {
--row-pad-top: var(--space-2, 4px);
--row-pad-bottom: var(--space-2, 4px);
--row-pad-left: var(--space-4, 12px);
--row-pad-right: var(--space-4, 12px);
--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, #fff);
overflow-x: auto;
width: 100%;
}

@media (prefers-color-scheme: dark) {
.expandable-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));
}
}

.table-header {
display: grid;
align-items: center;
padding: var(--row-pad-top) var(--row-pad-right) var(--row-pad-bottom) var(--row-pad-left);
background: var(--bgcolor-neutral-tertiary, #fff);
border-bottom: var(--border-width-s, 1px) solid var(--divider-strong);
height: var(--row-height);
box-sizing: border-box;
}
.header-cell {
display: flex;
align-items: center;
}

/* Responsive (maintain consistent height) */
@media (max-width: 480px) {
.table-header {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
padding: var(--row-pad-top) var(--space-2, 4px) var(--row-pad-bottom)
var(--space-2, 4px);
}
}
@media (min-width: 481px) and (max-width: 768px) {
.table-header {
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
padding: var(--row-pad-top) var(--space-3, 8px) var(--row-pad-bottom)
var(--space-3, 8px);
}
}
@media (min-width: 1200px) {
.table-header {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
padding: var(--row-pad-top) var(--space-6, 16px) var(--row-pad-bottom)
var(--space-6, 16px);
}
}
</style>
118 changes: 118 additions & 0 deletions v2/pink-sb/src/lib/expandable-table/row/Row.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<script lang="ts">
import { onMount } from 'svelte';
import { setContext } from 'svelte';
import type { RootProp } from '../index.js';
import { slide } from 'svelte/transition';

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

setContext('rowId', id);

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

$: isOpen = root.isOpen(id);
</script>

<div class="table-row" class:has-children={expandable} class:is-open={isOpen}>
<div class="row-content" style="grid-template-columns: {root.gridTemplateColumns};">
<slot />
</div>

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

<style lang="scss">
.table-row.has-children:not(.is-open):hover .row-content {
background: var(--overlay-hover);
}
.table-row.is-open .row-content {
background: var(--row-open-bg);
border-bottom-color: var(--divider-strong);
}

.row-content {
display: grid;
align-items: center;
padding: var(--row-pad-top) var(--row-pad-right) var(--row-pad-bottom) var(--row-pad-left);
border-bottom: var(--border-width-s, 1px) solid var(--divider-color);
height: var(--row-height);
box-sizing: border-box;
transition: background-color 0.2s ease;
}

.expanded-content {
background: var(--accordion-bg);
padding: 0;
position: relative;
}
.expanded-content::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: calc(var(--border-width-s, 1px) * 2);
background: var(--divider-strong);
box-shadow: 0 -1px 0 var(--divider-color) inset;
z-index: 1;
pointer-events: none;
}

:global(.child-row) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this expect the end user to always have a div or other element have this class? 🤔

display: grid;
align-items: center;
padding: var(--row-pad-top) var(--row-pad-right) var(--row-pad-bottom) var(--row-pad-left);
border-bottom: var(--border-width-s, 1px) solid var(--divider-color);
height: var(--row-height);
box-sizing: border-box;
background: transparent;
color: var(--fgcolor-neutral-secondary, rgba(0, 0, 0, 0.6));
}
:global(.child-row:last-child) {
border-bottom: none;
}
:global(.child-cell) {
display: flex;
align-items: center;
}

/* responsiveness */
@media (max-width: 480px) {
.row-content,
:global(.child-row) {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
padding: var(--row-pad-top) var(--space-2, 4px) var(--row-pad-bottom)
var(--space-2, 4px);
}
:global(.child-cell) {
gap: var(--space-1, 2px);
}
}
@media (min-width: 481px) and (max-width: 768px) {
.row-content,
:global(.child-row) {
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
padding: var(--row-pad-top) var(--space-3, 8px) var(--row-pad-bottom)
var(--space-3, 8px);
}
}
@media (min-width: 1200px) {
.row-content,
:global(.child-row) {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
padding: var(--row-pad-top) var(--space-6, 16px) var(--row-pad-bottom)
var(--space-6, 16px);
}
}
</style>
1 change: 1 addition & 0 deletions v2/pink-sb/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { default as AvatarGroup } from './avatar/AvatarGroup.svelte';
export { default as Badge } from './Badge.svelte';
export { default as Breadcrumbs } from './Breadcrumbs.svelte';
export { default as Divider } from './Divider.svelte';
export { default as Expandable } from './expandable-table/index.js';
export { default as FloatingActionBar } from './FloatingActionBar.svelte';
export { default as InteractiveText } from './InteractiveText.svelte';
export { default as Root } from './Root.svelte';
Expand Down
Loading
Loading