add: some nice stuff.

This commit is contained in:
Darshan
2025-09-16 19:42:34 +05:30
parent 594a673337
commit 3ed568d777
9 changed files with 524 additions and 43 deletions
+1 -1
View File
@@ -22,7 +22,7 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2407",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f4da718",
"@appwrite.io/pink-legacy": "^1.0.3",
+5 -5
View File
@@ -12,8 +12,8 @@ importers:
specifier: ^1.1.24
version: 1.1.24(svelte@5.25.3)(zod@3.24.3)
'@appwrite.io/console':
specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134
specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2407
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2407
'@appwrite.io/pink-icons':
specifier: 0.25.0
version: 0.25.0
@@ -260,8 +260,8 @@ packages:
'@analytics/type-utils@0.6.2':
resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134':
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2407':
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2407}
version: 1.10.0
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1':
@@ -3700,7 +3700,7 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2407': {}
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)':
dependencies:
@@ -10,7 +10,6 @@
import { createEventDispatcher } from 'svelte';
import { subNavigation } from '$lib/stores/database';
import Suggestions from './suggestions.svelte';
import type { TableColumnSuggestions } from './suggestions.svelte';
let {
showCreate = $bindable(false)
@@ -26,11 +25,6 @@
let touchedId = $state(false);
let error: string = $state(null);
let suggestions: TableColumnSuggestions = $state({
enabled: false,
context: null
})
const create = async () => {
error = null;
try {
@@ -111,7 +105,7 @@
}
}} />
<Suggestions bind:suggestions />
<Suggestions />
<svelte:fragment slot="footer">
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
@@ -1,5 +1,8 @@
<!-- TODO: Use Layout.Stack -->
<div class="ai-icon-holder">
<script lang="ts">
import { Layout } from '@appwrite.io/pink-svelte';
</script>
<Layout.Stack inline alignItems="center" justifyContent="center" class="ai-icon-holder">
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30" fill="none">
<path
d="M11.0156 6.20801C11.3539 5.29392 12.6461 5.29392 12.9844 6.20801L14.5801 10.5186C14.7776 11.052 15.198 11.4724 15.7314 11.6699L20.042 13.2656C20.9561 13.6039 20.9561 14.8961 20.042 15.2344L15.7314 16.8301C15.198 17.0276 14.7776 17.448 14.5801 17.9814L12.9844 22.292C12.6461 23.2061 11.3539 23.2061 11.0156 22.292L9.41992 17.9814C9.2224 17.448 8.80197 17.0276 8.26855 16.8301L3.95801 15.2344C3.04392 14.8961 3.04392 13.6039 3.95801 13.2656L8.26855 11.6699C8.80196 11.4724 9.2224 11.052 9.41992 10.5186L11.0156 6.20801Z"
@@ -49,20 +52,16 @@
</linearGradient>
</defs>
</svg>
</div>
</Layout.Stack>
<style lang="scss">
.ai-icon-holder {
:global(.ai-icon-holder) {
border-radius: 8px;
background: rgba(253, 54, 110, 0.04);
border: 1.25px solid rgba(253, 54, 110, 0.12);
gap: 12.5px;
width: 40px;
height: 40px;
padding: 5px 0;
display: flex;
align-items: center;
justify-content: center;
width: 40px !important;
height: 40px !important;
}
</style>
@@ -4,9 +4,21 @@ import type { Models } from '@appwrite.io/console';
import { derived, writable } from 'svelte/store';
import { IconChartBar, IconCloudUpload, IconCog } from '@appwrite.io/pink-icons-svelte';
export type TableColumnSuggestions = {
enabled: boolean;
thinking: boolean;
context?: string | null;
};
export const database = derived(page, ($page) => $page.data.database as Models.Database);
export const showCreateTable = writable(false);
export const tableColumnSuggestions = writable<TableColumnSuggestions>({
enabled: false,
context: null,
thinking: false
});
export const tableViewColumns = writable<Column[]>([
{ id: '$id', title: 'Table ID', type: 'string', width: 200 },
{ id: 'name', title: 'Name', type: 'string', width: { min: 120 } },
@@ -145,7 +145,10 @@
</Layout.Stack>
</div>
<Layout.Stack direction="column" gap="xxs" style="bottom: 1rem; position: sticky;">
<Layout.Stack
gap="xxs"
direction="column"
style="bottom: 1rem; position: relative; height: calc(20% - 1rem)">
<div class="action-menu-divider">
<Divider />
</div>
@@ -1,19 +1,14 @@
<script lang="ts">
import IconAI from './icon/ai.svelte';
import { slide } from 'svelte/transition';
import { tableColumnSuggestions } from './store';
import { InputTextarea } from '$lib/elements/forms';
import { Card, Layout, Selector, Typography } from '@appwrite.io/pink-svelte';
export type TableColumnSuggestions = {
enabled: boolean;
context?: string | null;
}
let {
suggestions = $bindable()
}: {
suggestions: TableColumnSuggestions
} = $props();
$effect(() => {
// TODO: update later, this is for mock only atm!
$tableColumnSuggestions.thinking = $tableColumnSuggestions.enabled;
});
</script>
<Card.Base variant="secondary" radius="s" padding="xs">
@@ -26,11 +21,12 @@
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary"
>Smart column suggestions</Typography.Text>
<Selector.Switch
id="suggestions"
label={undefined}
bind:checked={suggestions.enabled}
/>
<div class="suggestions-switch">
<Selector.Switch
id="suggestions"
label={undefined}
bind:checked={$tableColumnSuggestions.enabled} />
</div>
</Layout.Stack>
<Typography.Text color="--fgcolor-neutral-secondary">
@@ -39,17 +35,22 @@
</Layout.Stack>
</Layout.Stack>
{#if suggestions.enabled}
{#if $tableColumnSuggestions.enabled}
<div transition:slide={{ duration: 200 }}>
<InputTextarea
id="context"
rows={3}
maxlength={255}
autofocus
bind:value={suggestions.context}
placeholder="Optional: Add context to improve suggestions"
/>
bind:value={$tableColumnSuggestions.context}
placeholder="Optional: Add context to improve suggestions" />
</div>
{/if}
</Layout.Stack>
</Card.Base>
<style lang="scss">
.suggestions-switch :global(button) {
cursor: pointer;
}
</style>
@@ -29,8 +29,10 @@
import { IconChevronDown, IconChevronUp, IconPlus } from '@appwrite.io/pink-icons-svelte';
import type { Models } from '@appwrite.io/console';
import EmptySheet from './layout/emptySheet.svelte';
import SuggestionsEmptySheet from './layout/suggestionsEmptySheet.svelte';
import CreateRow from './rows/create.svelte';
import { onDestroy } from 'svelte';
import { tableColumnSuggestions } from '../store';
export let data: PageData;
@@ -215,6 +217,8 @@
}
}} />
{/if}
{:else if $tableColumnSuggestions.thinking}
<SuggestionsEmptySheet customColumns={$tableColumns} />
{:else}
<EmptySheet
mode="rows"
@@ -0,0 +1,468 @@
<script lang="ts">
import {
Button,
Icon,
Layout,
Spinner,
Spreadsheet,
Typography,
FloatingActionBar
} from '@appwrite.io/pink-svelte';
import { IconCalendar, IconFingerPrint, IconPlus } from '@appwrite.io/pink-icons-svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import { SortButton } from '$lib/components';
import type { Column } from '$lib/helpers/types';
import { expandTabs } from '../store';
import SpreadsheetContainer from './spreadsheet.svelte';
import { onDestroy, onMount } from 'svelte';
import { debounce } from '$lib/helpers/debounce';
import { tableColumnSuggestions } from '../../store';
const { customColumns = [] }: { customColumns?: Column[] } = $props();
// can also be `__select_undefined` if `$id` needs to be covered on overlay!
const firstColumn = '$id';
let resizeObserver: ResizeObserver;
let spreadsheetContainer: HTMLElement;
let hScroller: HTMLElement | null = null;
let headerElement: HTMLElement | null = null;
let rangeOverlayEl: HTMLDivElement | null = null;
const baseColProps = { draggable: false, resizable: false };
const findHorizontalScroller = (root: HTMLElement | null): HTMLElement | null => {
let el = root as HTMLElement | null;
while (el && el !== document.body) {
const st = getComputedStyle(el);
if (
(st.overflowX === 'auto' || st.overflowX === 'scroll') &&
el.scrollWidth > el.clientWidth
) {
return el;
}
el = el.parentElement as HTMLElement | null;
}
return null;
};
const updateOverlayHeight = () => {
if (!spreadsheetContainer) return;
if (!headerElement || !headerElement.isConnected) {
headerElement = spreadsheetContainer.querySelector('[role="rowheader"]');
}
if (!headerElement) return;
const headerRect = headerElement.getBoundingClientRect();
const containerRect = spreadsheetContainer.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const dynamicHeightPx = viewportHeight - headerRect.bottom;
const overlayTop = Math.round(headerRect.bottom - containerRect.top);
const overlayHeight = $expandTabs
? `${dynamicHeightPx}px`
: `calc(${dynamicHeightPx}px - 89px)`;
spreadsheetContainer.style.setProperty('--overlay-top', `${overlayTop}px`);
spreadsheetContainer.style.setProperty('--overlay-height', overlayHeight);
};
// Measure first content header
// and the cell before `actions`|`empty`
const updateOverlayBounds = () => {
if (!spreadsheetContainer) return;
if (!headerElement || !headerElement.isConnected) {
headerElement = spreadsheetContainer.querySelector('[role="rowheader"]');
if (!headerElement) return;
}
// hook the actual horizontal scroller once
if (!hScroller || !hScroller.isConnected) {
hScroller = findHorizontalScroller(headerElement);
if (hScroller) hScroller.addEventListener('scroll', debouncedRecalc, { passive: true });
}
const containerRect = spreadsheetContainer.getBoundingClientRect();
const getById = (id: string) =>
headerElement!.querySelector<HTMLElement>(
`[role="cell"][data-header="true"][data-column-id="${id}"]`
);
const firstColumnCell = getById(firstColumn);
const actionsCell = getById('actions');
const emptyCell = getById('empty');
// start = first content cell after `firstColumn`
// (skip actions/empty if they were there)
let startCell: HTMLElement | null = null;
if (firstColumnCell) {
let node = firstColumnCell.nextElementSibling as HTMLElement | null;
while (
node &&
(node.dataset.columnId === 'actions' || node.dataset.columnId === 'empty')
) {
node = node.nextElementSibling as HTMLElement | null;
}
startCell = node;
}
// end = cell right BEFORE actions;
// else right BEFORE empty; else last content cell
const before = (el: HTMLElement | null) => {
if (!el) return null;
let previous = el.previousElementSibling as HTMLElement | null;
while (
previous &&
(previous.dataset.columnId === firstColumn ||
previous.getAttribute('data-select') === 'true')
) {
previous = previous.previousElementSibling as HTMLElement | null;
}
return previous;
};
let endCell = before(actionsCell) ?? before(emptyCell);
if (!endCell) {
const cells = Array.from(
headerElement.querySelectorAll<HTMLElement>('[role="cell"][data-header="true"]')
);
for (let index = cells.length - 1; index >= 0; index--) {
const cell = cells[index];
const cellId = cell.dataset.columnId;
const isSelect = cell.getAttribute('data-select') === 'true';
if (!isSelect && cellId !== firstColumn && cellId !== 'actions' && cellId !== 'empty') {
endCell = cell;
break;
}
}
}
if (!startCell || !endCell) {
if (rangeOverlayEl) rangeOverlayEl.style.display = 'none';
return;
}
const start = startCell.getBoundingClientRect();
const end = endCell.getBoundingClientRect();
const left = Math.round(start.left - containerRect.left);
const width = Math.max(0, Math.round(end.right - end.left));
spreadsheetContainer.style.setProperty('--group-left', `${left}px`);
spreadsheetContainer.style.setProperty('--group-width', `${width}px`);
if (rangeOverlayEl) rangeOverlayEl.style.display = width > 0 ? 'block' : 'none';
};
const recalcAll = () => {
updateOverlayHeight();
updateOverlayBounds();
};
const debouncedRecalc = debounce(recalcAll, 50);
onMount(() => {
if (spreadsheetContainer) {
resizeObserver = new ResizeObserver(debouncedRecalc);
resizeObserver.observe(spreadsheetContainer);
}
requestAnimationFrame(recalcAll);
});
onDestroy(() => {
resizeObserver?.disconnect();
hScroller?.removeEventListener('scroll', debouncedRecalc);
});
const getCustomColumns = (): Column[] =>
customColumns.map((col: Column) => ({ ...col, width: 180, hide: false, ...baseColProps }));
const getRowColumns = (): Column[] => [
{
id: '$id',
title: '$id',
type: 'string',
width: 180,
icon: IconFingerPrint,
...baseColProps
},
...getCustomColumns(),
{
id: '$createdAt',
title: '$createdAt',
type: 'datetime',
width: 180,
icon: IconCalendar,
...baseColProps
},
{
id: '$updatedAt',
title: '$updatedAt',
type: 'datetime',
width: 180,
icon: IconCalendar,
...baseColProps
},
{
id: 'actions',
title: '',
type: 'string',
icon: IconPlus,
width: customColumns.length ? 555 : 832,
...baseColProps
},
{ id: 'empty', title: '', type: 'string', ...baseColProps }
];
const spreadsheetColumns = getRowColumns();
const emptyCells = $derived(($isSmallViewport ? 14 : 17) + (!$expandTabs ? 2 : 0));
</script>
<svelte:window on:resize={debouncedRecalc} on:scroll={debouncedRecalc} />
<div bind:this={spreadsheetContainer} class="databases-spreadsheet spreadsheet-container-outer">
<div>
<div
aria-hidden="true"
bind:this={rangeOverlayEl}
class="columns-range-overlay"
class:thinking={$tableColumnSuggestions.thinking}>
</div>
{#if $tableColumnSuggestions.thinking}
<div class="floating-action-wrapper">
<FloatingActionBar>
<svelte:fragment slot="start">
<Layout.Stack direction="row" gap="xxs" alignItems="center">
<Spinner size="s" />
<Typography.Text style="white-space: nowrap">
Thinking of column suggestions
</Typography.Text>
</Layout.Stack>
</svelte:fragment>
<svelte:fragment slot="end">
<Button.Button
size="xs"
variant="secondary"
on:click={() => {
$tableColumnSuggestions.context = null;
$tableColumnSuggestions.enabled = false;
$tableColumnSuggestions.thinking = false;
}}
>Cancel
</Button.Button>
</svelte:fragment>
</FloatingActionBar>
</div>
{/if}
</div>
<SpreadsheetContainer>
<Spreadsheet.Root
{emptyCells}
allowSelection
height="100%"
columns={spreadsheetColumns}
bottomActionClick={() => {}}>
<svelte:fragment slot="header" let:root>
{#each spreadsheetColumns as column (column.id)}
<Spreadsheet.Header.Cell
{root}
column={column.id}
icon={column.icon ?? undefined}>
{#if column.isAction}
<Button.Button icon variant="extra-compact">
<Icon icon={IconPlus} color="--fgcolor-neutral-primary" />
</Button.Button>
{:else if column.id === 'actions' || column.id === 'empty'}
{column.title}
{:else}
<Layout.Stack
gap="xs"
direction="row"
alignItems="center"
alignContent="center">
{column.title}
<SortButton disabled column={column.id} />
</Layout.Stack>
{/if}
</Spreadsheet.Header.Cell>
{/each}
</svelte:fragment>
</Spreadsheet.Root>
</SpreadsheetContainer>
<div
class="spreadsheet-fade-bottom"
data-collapsed-tabs={!$expandTabs}
style="height: var(--overlay-height)">
</div>
</div>
<style lang="scss">
.spreadsheet-container-outer {
width: 100%;
position: fixed;
overflow: hidden;
& :global([role='rowheader'] :nth-last-child(2) [role='presentation']) {
display: none;
}
.columns-range-overlay {
width: 100%;
position: absolute;
margin-block-start: 2px;
left: var(--group-left, 0px);
height: calc(var(--overlay-height, 60vh) + 36px);
border-radius: var(--border-radius-S, 8px);
background: color-mix(in oklab, #fd366e 7%, transparent);
pointer-events: none;
z-index: 21;
overflow: hidden;
/* border + inner hairline without double-counting */
box-shadow:
0 0 0 var(--border-width-L, 2px) #fd366e,
inset 0 0 0 1px color-mix(in oklab, #fe9567 20%, transparent);
/* pretty gradient wash (with fallback) */
background: rgba(253, 54, 110, 0.07);
@supports (background: color-mix(in oklab, #fd366e 7%, transparent)) {
background: linear-gradient(
135deg,
color-mix(in oklab, #fd366e 7%, transparent) 0%,
color-mix(in oklab, #fe9567 7%, transparent) 100%
);
}
&.thinking {
&::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
border-radius: inherit;
padding: 2px;
background: linear-gradient(
90deg,
rgba(253, 54, 110, 0.02),
rgba(254, 149, 103, 0.05),
rgba(253, 54, 110, 0.02),
rgba(254, 149, 103, 0.05),
rgba(253, 54, 110, 0.02)
);
background-size: 400% 100%;
animation: border-shimmer 4s ease-in-out infinite;
z-index: -1;
}
&::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.08),
transparent
);
animation: inner-shimmer 4s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
z-index: 1;
}
}
}
& .floating-action-wrapper :global(:first-child) {
left: 55%;
z-index: 21;
}
& :global(.spreadsheet-container) {
overflow-x: hidden;
overflow-y: hidden;
}
& :global([data-select='true']) {
opacity: 0.85;
pointer-events: none;
}
}
.spreadsheet-fade-bottom {
bottom: 0;
width: 100%;
position: fixed;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.855) 21%,
#fff 100%
);
z-index: 20; /* under overlay */
display: flex;
justify-content: center;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
:global(.theme-dark) .spreadsheet-fade-bottom {
background: linear-gradient(
180deg,
rgba(25, 25, 28, 0.38) 13%,
rgba(25, 25, 28, 0.7) 21%,
rgba(25, 25, 28, 0.95) 38%,
var(--bgcolor-neutral-default, #19191c) 100%
);
}
@keyframes border-shimmer {
0% {
background-position: 400% 0;
}
50% {
background-position: -50% 0;
}
100% {
background-position: -400% 0;
}
}
@keyframes inner-shimmer {
0% {
left: -100%;
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
left: 100%;
opacity: 0;
}
}
:global(.theme-dark) .columns-range-overlay.thinking {
&::before {
background: linear-gradient(
90deg,
rgba(253, 54, 110, 0.01),
rgba(254, 149, 103, 0.03),
rgba(253, 54, 110, 0.01),
rgba(254, 149, 103, 0.03),
rgba(253, 54, 110, 0.01)
);
}
&::after {
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.04), transparent);
}
}
</style>