Initial open source release

This commit is contained in:
Ronni Skansing committed 2025-08-21 16:14:09 +02:00
commit 11cf01f08e
488 files changed
+97180

No files matched your search

@@ -0,0 +1,13 @@
<script>
import { onClickCopy } from '$lib/utils/common';
export let text;
</script>
<button
class="hover:bg-gray-100 px-2 py-1 rounded-md transition-colors w-full text-left text-ellipsis overflow-hidden"
title={text}
on:click={() => onClickCopy(text)}
>
{text}
</button>
@@ -0,0 +1,16 @@
<script>
/** @type {*} */
export let page = null;
export let plural;
export let colspan = 100;
</script>
<tr class="text-center bg-pleasant-gray">
<td class="p-24" {colspan}>
{#if page === 1}
<p class="text-lg text-gray-600">No {plural} found</p>
{:else}
<p class="text-lg text-gray-600">No more results found</p>
{/if}
</td>
</tr>
@@ -0,0 +1,16 @@
<script>
import { toEvent } from '$lib/utils/events';
import { onMount } from 'svelte';
export let eventName;
let event = { name: '', priority: null, color: null };
onMount(() => {
event = toEvent(eventName);
});
</script>
<div class="flex items-center text-ellipsis">
<div class="w-4 h-4 {event.color} mr-2 rounded-sm"></div>
{event.name}
</div>
@@ -0,0 +1,92 @@
<script>
import EmptyTableResult from './EmptyTableResult.svelte';
import Pagination from '../Pagination.svelte';
import TableHeader from './TableHeader.svelte';
import Search from '$lib/components/Search.svelte';
import Select from '$lib/components/Select.svelte';
import { afterUpdate, onMount } from 'svelte';
import TableCell from './TableCell.svelte';
import TableRow from './TableRow.svelte';
import TableCellEmpty from './TableCellEmpty.svelte';
import TableCellAction from './TableCellAction.svelte';
import GhostText from '../GhostText.svelte';
import { scrollBarClassesHorizontal } from '$lib/utils/scrollbar';
/** @type {Array<string>|*} */
export let columns = [];
/** @type {boolean} */
export let hasData;
/** @type {string} */
export let plural;
/** @type {*} */
export let pagination = null;
export let sortable = [];
// key value map that should be switched on when selecting a sort by
export let hasActions = true;
export let isGhost = false;
let tableWrapper = null;
let columnsLength = columns.length;
let rowsLength = 0;
afterUpdate(() => {
const elements = tableWrapper?.querySelectorAll('table > tr.table-row');
rowsLength = elements?.length ?? 0;
});
onMount(() => {
if (!pagination && sortable?.length) {
console.warn('You need to pass a pagination object to make the column sortable');
}
columnsLength = columns.length + 2;
});
let currentPage = pagination && pagination.currentPage;
</script>
<div>
<div class="">
<div class="flex justify-between items-center pb-4">
{#if pagination}
<Select {pagination}></Select>
<Search {pagination}></Search>
{/if}
</div>
<div
bind:this={tableWrapper}
class="
border-2 rounded-md px-4 py-4 overflow-x-auto
{scrollBarClassesHorizontal}"
>
<table class="w-full table-fixed" class:animate-pulse={isGhost}>
<TableHeader {isGhost} {columns} {sortable} {hasActions} {pagination} />
{#if !hasData && !isGhost}
<EmptyTableResult page={currentPage} {plural} colspan={columnsLength} />
{/if}
{#if !isGhost}
<slot />
{:else}
{#each Array(rowsLength || pagination?.perPage) as _, row}
<TableRow>
{#each columns as column}
<TableCell>
<GhostText />
</TableCell>
{/each}
<TableCellEmpty />
<TableCellAction>
<GhostText square center />
</TableCellAction>
</TableRow>
{/each}
{/if}
</table>
</div>
{#if pagination}
<Pagination paginator={pagination} />
{:else}
<div class="flex items-center mb-8 mt-4" />
{/if}
</div>
</div>
@@ -0,0 +1,30 @@
<!-- TableCell.svelte -->
<script>
import Datetime from '../Datetime.svelte';
import RelativeTime from '../RelativeTime.svelte';
/** @type {string} */
export let value = '';
export let isDate = false;
export let hideHours = false;
export let isRelative = false; // new prop to toggle relative time
</script>
<td
class={`pl-4 font-regular text-slate-600 text-ellipsis whitespace-nowrap overflow-hidden pr-4`}
title={isDate ? '' : value}
>
{#if value}
{#if isDate}
{#if isRelative}
<RelativeTime {value} />
{:else}
<Datetime {value} {hideHours} />
{/if}
{:else}
{value ?? ''}
{/if}
{:else}
<slot />
{/if}
</td>
@@ -0,0 +1,5 @@
<td class="pl-4 w-48 text-center border: hidden;">
<p class="font-regular text-slate-600">
<slot />
</p>
</td>
@@ -0,0 +1,5 @@
<td class="pl-4 w-40 text-center border: hidden;">
<p class="font-regular text-slate-600 flex justify-center">
<slot />
</p>
</td>
@@ -0,0 +1,3 @@
<td class="pl-4 w-4 border: hidden;">
</td>
@@ -0,0 +1,12 @@
<script>
import TableDropDownButton from './TableDropDownButton.svelte';
export let disabled = false;
export let title = 'Copy';
if (disabled && !title) {
title = 'Disabled';
}
</script>
<TableDropDownButton name={'Copy'} {title} on:click {disabled} />
@@ -0,0 +1,22 @@
<script>
export let disabled = false;
export let title = '';
export let name = 'Delete';
if (disabled && !title) {
title = 'Delete is disabled';
}
</script>
{#if disabled}
<button class="px py text-slate-300 cursor-not-allowed" {disabled} {title}>
<p class="ml-2 text-left">{name}</p>
</button>
{:else}
<button
class="px py-1 text-slate-600 hover:bg-red-400 hover:text-white cursor-pointer"
on:click
{title}
>
<p class="ml-2 text-left">{name}</p>
</button>
{/if}
@@ -0,0 +1,31 @@
<script>
export let disabled = false;
export let title = '';
export let name;
if (disabled && !title) {
title = 'Disabled';
}
const handleKeydown = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
e.target.click();
}
};
</script>
{#if disabled}
<button class="px py-1 text-slate-300 cursor-not-allowed" {disabled} {title}>
<p class="ml-2 text-left">{name}</p>
</button>
{:else}
<button
class="px py-1 text-slate-600 hover:bg-highlight-blue hover:text-white cursor-pointer"
on:click
on:keydown={handleKeydown}
{title}
>
<p class="ml-2 text-left">{name}</p>
</button>
{/if}
@@ -0,0 +1,133 @@
<script>
import { onMount, onDestroy } from 'svelte';
import { activeFormElement } from '$lib/store/activeFormElement';
import { scrollBarClassesVertical } from '$lib/utils/scrollbar';
let isMenuVisible = false;
let menuX = 0;
let menuY = 0;
let menuRef = null;
let buttonRef = null;
// generate unique ID for this dropdown instance
const dropdownId = Symbol();
// subscribe to active dropdown store
const unsubscribe = activeFormElement.subscribe((activeId) => {
isMenuVisible = activeId === dropdownId;
});
const toggle = (e) => {
if (isMenuVisible) {
activeFormElement.set(null);
} else {
document.addEventListener('click', handleClickWhenVisible);
document.addEventListener('keydown', handleGlobalKeydown);
activeFormElement.set(dropdownId); // set this as active, closing others
const viewportHeight = window.innerHeight;
const menuHeight = 128; // max-h-32 in pixels
const buffer = 20; // extra space to ensure some padding from viewport edges
let clickViewportY, pageX, pageY;
// Handle both mouse and keyboard events
if (e.clientY !== undefined && e.pageX !== undefined) {
// Mouse event
clickViewportY = e.clientY;
pageX = e.pageX;
pageY = e.pageY;
} else {
// Keyboard event - use button position
const buttonRect = buttonRef.getBoundingClientRect();
clickViewportY = buttonRect.top;
pageX = buttonRect.left + window.scrollX;
pageY = buttonRect.top + window.scrollY;
}
// is the room enough to show the box
const shouldShowAbove = viewportHeight - clickViewportY < menuHeight + buffer;
// find position
menuX = pageX - 192;
menuY = shouldShowAbove
? pageY - menuHeight // Position above click
: pageY; // Position below click
menuRef.style = `left: ${menuX}px; top: ${menuY}px`;
}
};
const handleClickWhenVisible = (event) => {
if (isMenuVisible && menuRef && buttonRef) {
activeFormElement.set(null);
}
event.preventDefault();
event.stopPropagation();
document.removeEventListener('click', handleClickWhenVisible);
};
const handleGlobalKeydown = (event) => {
if (event.key === 'Escape' && isMenuVisible) {
activeFormElement.set(null);
document.removeEventListener('keydown', handleGlobalKeydown);
}
};
const handleKeydown = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
toggle(e);
} else if (e.key === 'Escape' && isMenuVisible) {
e.preventDefault();
e.stopPropagation();
activeFormElement.set(null);
}
};
const _onDestroy = () => {
document.removeEventListener('click', handleClickWhenVisible);
document.removeEventListener('keydown', handleGlobalKeydown);
unsubscribe();
// Clear active dropdown if this one was active
activeFormElement.update((current) => (current === dropdownId ? null : current));
};
onDestroy(_onDestroy);
onMount(() => {
return () => {
_onDestroy();
};
});
</script>
<div class="">
<button
bind:this={buttonRef}
class="py-2 px-2"
on:click|stopPropagation|preventDefault={toggle}
on:keydown={handleKeydown}
>
<svg width="3.335557" height="16.465519" viewBox="0 0 0.88253281 4.3565019">
<g transform="translate(-892.25669,88.863024)">
<g transform="matrix(0,1.0139418,-1.0139418,0,802.48114,-807.2715)">
<circle class="fill-cta-blue" cx="708.99603" cy="-88.976357" r="0.40846577" />
<circle class="fill-cta-blue" cx="710.67859" cy="-88.976357" r="0.40846577" />
<circle class="fill-cta-blue" cx="712.36115" cy="-88.976357" r="0.40846577" />
</g>
</g>
</svg>
</button>
<div
bind:this={menuRef}
class="absolute bg-white drop-shadow-md z-20 max-h-32 w-48 rounded-md overflow-y-scroll {scrollBarClassesVertical}"
class:hidden={!isMenuVisible}
>
<ul class="flex flex-col text-left">
<slot />
</ul>
</div>
</div>
@@ -0,0 +1,3 @@
<thead class="table-header-group">
<slot />
</thead>
@@ -0,0 +1,81 @@
<script>
import { onMount } from 'svelte';
import GhostText from '../GhostText.svelte';
export let column;
/** @type {*|null} */
export let pagination = null;
export let alignText = 'left';
export let sortable = false;
export let size = '';
// last tells if it the last field column before the actions column
export let last = false;
export let fillRest = false;
export let isGhost = false;
export let title = '';
let sortBy = pagination?.sortBy;
let sortOrder = pagination?.sortOrder;
const setSortAndSortBy = () => {
if (!sortable || !pagination) {
return;
}
pagination.sort(column.toLowerCase(), sortOrder.toLowerCase());
};
onMount(() => {
if (pagination) {
pagination.onChange(() => {
sortBy = pagination.sortBy;
sortOrder = pagination.sortOrder;
});
}
});
</script>
<th
class="pl-4 bg-grayblue-light py-4 border-hidden first:rounded-tl-lg first:rounded-bl-lg last:rounded-tr-lg last:border-4 min-w-48"
class:rounded-br-lg={last}
class:rounded-tr-lg={last}
class:w-48={size === 'small'}
class:w-56={size === 'medium'}
class:w-80={size === 'large'}
class:w-full={fillRest}
>
<button
class="flex group cursor-pointer"
class:pointer-events-none={!sortable}
class:table-cell={alignText === 'center'}
on:click|preventDefault={setSortAndSortBy}
>
<div class="w-full">
<p class="font-bold text-slate-600 text-{alignText} flex">
{#if !isGhost}
{title.length ? title : column}
{:else}
<GhostText />
{/if}
{#if sortable && column.toLowerCase() === sortBy.toLowerCase() && !isGhost}
<div
class:bg-transparent={sortOrder === ''}
class="flex justify-center items-center w-6 h-6 ml-2 rounded-md bg-cta-blue"
>
{#if sortOrder === 'asc'}
<div>
<img src="/arrow-up.svg" alt="arrow up" />
</div>
{/if}
{#if sortOrder === 'desc'}
<div>
<img src="/arrow-down.svg" alt="arrow down" />
</div>
{/if}
{#if sortOrder === ''}
<p class="text-white"></p>
{/if}
</div>
{/if}
</p>
</div>
</button>
</th>
@@ -0,0 +1,5 @@
<th class="bg-cta-light-blue w-20 py-4 border-hidden rounded-lg">
<p class="font-bold text-slate-600 text-center">
<slot />
</p>
</th>
@@ -0,0 +1,5 @@
<th class="pl-4 bg-grayblue-light w-48 py-4 border-hidden">
<p class="font-bold text-slate-600 flex justify-center text-center">
<slot />
</p>
</th>
@@ -0,0 +1,3 @@
<th class="pl-4 bg-white w-4 py-4 border-hidden">
</th>
@@ -0,0 +1,5 @@
<th class="pl-4 bg-grayblue-light w-48 py-4 border-hidden rounded-md">
<p class="font-bold text-slate-600 text-left">
<slot />
</p>
</th>
@@ -0,0 +1,62 @@
<script>
import { onMount } from 'svelte';
import TableHead from './TableHead.svelte';
import TableHeadCell from './TableHeadCell.svelte';
import TableHeadCellAction from './TableHeadCellAction.svelte';
import TableHeadCellEmpty from './TableHeadCellEmpty.svelte';
import TableRow from './TableRow.svelte';
import TableRowEmpty from './TableRowEmpty.svelte';
import GhostText from '../GhostText.svelte';
export let columns = [];
export let sortable = [];
export let isGhost = false;
export let hasActions = true;
/** @type {*|null} */
export let pagination = null;
$: sortableMap = {};
onMount(() => {
sortable.forEach((column) => {
sortableMap[column.toLowerCase()] = true;
});
});
</script>
<TableHead>
<TableRow>
{#each columns as column, i (i)}
{#if typeof column === 'object'}
<TableHeadCell
{...column}
{...columns.length === 1 ? { size: '' } : {}}
{pagination}
{isGhost}
sortable={sortableMap[column.column.toLowerCase()]}
last={i === columns.length - 1}
fillRest={i === columns.length - 1 && !hasActions}
/>
{:else}
<TableHeadCell
{column}
{pagination}
sortable={sortableMap[column.toLowerCase()]}
last={i === columns.length - 1}
fillRest={i === columns.length - 1 && !hasActions}
/>
{/if}
{/each}
{#if hasActions}
<TableHeadCellEmpty />
<TableHeadCellAction>
{#if !isGhost}
Actions
{:else}
<GhostText center />
{/if}
</TableHeadCellAction>
{/if}
</TableRow>
<TableRowEmpty />
</TableHead>
@@ -0,0 +1,3 @@
<tr class="table-row odd:bg-white even:bg-pleasant-gray h-16 hover:bg-cta-light-blue">
<slot />
</tr>
@@ -0,0 +1,3 @@
<tr class="table-row bg-white h-4">
<slot />
</tr>
@@ -0,0 +1,12 @@
<script>
import TableDropDownButton from './TableDropDownButton.svelte';
export let disabled = false;
export let title = '';
export let name = 'Update';
if (disabled && !title) {
title = 'Disabled';
}
</script>
<TableDropDownButton {name} {title} on:click {disabled} />
@@ -0,0 +1,12 @@
<script>
import TableDropDownButton from './TableDropDownButton.svelte';
export let disabled = false;
export let title = '';
export let name = 'View';
if (disabled && !title) {
title = 'Disabled';
}
</script>
<TableDropDownButton {name} {title} on:click {disabled} />