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
1 change: 0 additions & 1 deletion .github/workflows/codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,3 @@ jobs:
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}

18 changes: 18 additions & 0 deletions src/lib/components/color-picker/color-picker.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
<!--
# Color Picker component

Provides input for the Color type.
The user can either choose from a pre-defined palette or use the system color picker.
The latter only works in browser that support the HTML5 color input type.

Props:
- `color` (required): The currently selected color. Can be used with `bind`.
- `options`: An array of [Color, string] pairs, where the string is a human-readable name for the color (e.g. "Cyan").
This is the pre-defined palette. Defaults to a list of 8 colors based on Tailwind CSS defaults.
- `onSelect`: A callback function that is called when a color is selected.
- `class`: String of additional Tailwind CSS classes to add to the top-level div.

See also:
- https://www.npmjs.com/package/color
- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/color
-->
<!--suppress ES6UnusedImports -->
<script lang="ts">
import { Button } from "$lib/components/ui/button";
Expand Down
30 changes: 29 additions & 1 deletion src/lib/components/combobox/combobox.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
<!--
# Generic Combobox Component

This is a generic combobox (i.e. Select with a built-in search) that is used to pick ONE item from a list.
The items can be any type, but to make it work you must supply your own functions to:
- Provide a human-friendly name (and, optionally, a picture, icon, and/or description tooltip) for each item. (`display`)
- Generate a unique ID for each item (can be any string, as long as it is unique and consistent). (`getId`)

By default, the item ID is used to compare items (needed to mark the currently selected item with a checkmark).
You can override `eq` with your own implementation. Idk why you would want to do that, but the option is there.

## Props:
- `options` (required): The list of items to choose from. Can be any type T.
- `display` (required): A function that takes an option (`T` or `undefined`) and returns a `Display` object.
- `getId` (required): A function that takes an option (`T`) and returns a string ID for that option.
- `eq`: A function that takes two options (`T` or `undefined`) and returns true if they are equal. Default: calls `getId` on both and compares the results.
- `value`: The currently selected item. Must be type `T` or `undefined` (meaning no item is selected). Bindable; Defaults to `undefined` (no item selected).
- `open`: Whether the combobox is open or closed. Bindable; Defaults to `false` (closed).
- `onChange`: A callback that fires when the selected item changes. Takes the old and new values as arguments. Defaults to a no-op.
- `onSelect`: A callback that fires when the user selects an item. Takes the new value as an argument. Defaults to a no-op.
- `allowUnselect`: If true, the user can unselect the currently selected item (i.e. set it to `undefined`). Defaults to true.
- `closeOnSelect`: If true, the combobox will close when the user selects an item. Defaults to true.
- `placeholder`: A string to show when no item is selected. Defaults to "Select an item".
- `class`: A string of additional Tailwind CSS classes to add to the top-level div.

See also:
- $lib/ui/Display - the Display interface definition
-->
<!--suppress ES6UnusedImports -->
<script lang="ts" generics="T">
import { Button } from "$lib/components/ui/button";
Expand All @@ -23,7 +51,7 @@
export let allowUnselect = true;
export let closeOnSelect = true;
export let value: T | undefined = undefined;
export let placeholder: string | undefined = undefined;
export let placeholder = "Select an item";
export let options: T[] = [];
let className = "";

Expand Down
43 changes: 19 additions & 24 deletions src/lib/components/corner-highlight/corner-highlight.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
<!--
# Color Highlight Component

Helper component that wraps the given content in a rectangular div with one corner highlighted by a colored triangle.
Designed for use inside of tables, cards, or similar.

## Props:
- `highlightColor`: The color of the highlight. Can be a string or a Color object. Defaults to "#ff3e00" (orange).
- `cornerSize`: Size of the corner highlight. Can be a CSS string (e.g. "30px", "2rem") or a number (e.g. 30 - equivalent to "30px"). Defaults to "30px".
- `cornerOpacity`: Opacity of the corner highlight. Defaults to 1 (fully opaque).
- `position`: Which corner to highlight. Defaults to `top-left`.
- `contentClass`: Additional Tailwind classes to apply to the content area.
- `class`: Additional Tailwind classes to apply to the container.
-->

<script lang="ts">
import Color from "color";

Expand Down Expand Up @@ -40,26 +55,11 @@

switch (position) {
case "top-left":
return {
...baseStyles,
top: 0,
left: 0,
clipPath: "polygon(0 0, 100% 0, 0 100%)",
};
return { ...baseStyles, top: 0, left: 0, clipPath: "polygon(0 0, 100% 0, 0 100%)" };
case "top-right":
return {
...baseStyles,
top: 0,
right: 0,
clipPath: "polygon(100% 0, 0 0, 100% 100%)",
};
return { ...baseStyles, top: 0, right: 0, clipPath: "polygon(100% 0, 0 0, 100% 100%)" };
case "bottom-left":
return {
...baseStyles,
bottom: 0,
left: 0,
clipPath: "polygon(0 100%, 100% 100%, 0 0)",
};
return { ...baseStyles, bottom: 0, left: 0, clipPath: "polygon(0 100%, 100% 100%, 0 0)" };
case "bottom-right":
return {
...baseStyles,
Expand All @@ -68,12 +68,7 @@
clipPath: "polygon(100% 100%, 100% 0, 0 100%)",
};
default:
return {
...baseStyles,
top: 0,
left: 0,
clipPath: "polygon(0 0, 100% 0, 0 100%)",
};
return { ...baseStyles, top: 0, left: 0, clipPath: "polygon(0 0, 100% 0, 0 100%)" };
}
}

Expand Down
48 changes: 48 additions & 0 deletions src/lib/components/data-table/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
This directory contains a wrapper over the `shadcn-svelte` [Data Table](https://www.shadcn-svelte.com/docs/components/data-table) component,
which, in turn, wraps [Svelte Headless Table](https://svelte-headless-table.bryanmylee.com/).

The underlying component is "headless" (i.e. provides the underlying logic without display), and we define the styling here.
We also set up the "plugins" (e.g. search, sorting, pagination) and define columns for data types used in this project.

The base logic is in `core`, utilities are in `lib` and concrete implementations are here (e.g. `person-data-table.svelte`).

Thanks to the headless component, we can easily define columns in a JSON format:

```typescript
let columnInitializers: ColumnInitializer<Shift>[] = [
{
accessor: (row) => row as Display,
cell: (cell) => createRender(ProfilePicture, { item: cell.value }),
header: "Icon",
id: "icon",
plugins: {
sort: {
disable: true,
},
},
},
{
accessor: (row: Person) => dobFormatter.format(row.dob),
header: "Date of Birth",
id: "dob",
},
...
]
```

And provide a dropdown of actions for each row (e.g. editing or deleting the underlying entry):

```typescript
let actions = new Map([
["Edit", rowClick],
["Delete", rowDelete],
]);
function rowDelete(item: Person) {
// do something on click!
}
```

And everything else is handled for us by the library and the `core` implementation!

This reduces the complexity and boilerplate required to write proper data tables.
Data tables are used widely in the project. If you are looking to build a new one, I would suggest copying one of the existing implementations and editing the columns / row actions.
7 changes: 7 additions & 0 deletions src/lib/components/data-table/core/actions-btn.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
<!--
Dropdown menu that is displayed at the end of each row.

Props:
- `item`: The item this row represents.
- `actions`: A map of human-readable names to functions that take the item as an argument.
-->
<!--suppress ES6UnusedImports -->
<script lang="ts" generics="T">
import { Button } from "$lib/components/ui/button";
Expand Down
92 changes: 82 additions & 10 deletions src/lib/components/data-table/core/data-table.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,87 @@
<!-- Need this due to the way the svelte-headless-table library is written -->
<!--
# Data Table (Base Component)

Provides the basic styling and functionality for a data table. Wraps the `svelte-headless-table` library.

## Props
- `data`: A Svelte store containing a list of items to display in the table. Can be any type `T[]`.
- `header`: True if there is a toolbar immediately above the table. Only used for styling.
- `actions`: A map of human-readable names to functions that take the item as an argument. Used to create a dropdown menu at the end of each row.
- `defaultAction`: A function that takes the item as an argument. Called when the user clicks on a row.
- `columnInitializers`: An array of column initializers. See below for more details, and also check the `svelte-headless-table` docs.
- `paginationConfig`: Pagination parameters (e.g. page size, initial page index, etc.). See `svelte-headless-table` docs for more details.
- `sortByConfig`: Sort plugin config. Can be used to set some initial sort keys, among other things. See `svelte-headless-table` docs for more details.
- `className`: Additional Tailwind CSS classes to add to the table.
- `filterValue`: Svelte store containing the current search string; Used to filter table rows. Table updates reactively when this changes.
- `hideForId`: An object with column IDs as keys and booleans as values. A column is hidden iff its ID is in the object and set to true.
- `selectedDataIds`: Set of IDs of currently selected rows. Not currently used. See `svelte-headless-table` docs for more details.
- `sortKeys`: State of the sort plugin, including a list of currently active sort keys. See `svelte-headless-table` docs for more details.
- `flatColumns`: Column information generated by the library. Pass this to plugin functions and components that invoke them (or otherwise need to know about the columns).

## Column Initializers

Used to create columns in the table. Each column is given by an object in the form:

```typescript
{
accessor: (row: T) => any, // Function that takes an item and returns a value for this column.
cell: (cell: Cell<T>) => createRender(MyComponent, { item: cell.value, ... }) // Optional function to render an arbitrary component in the cell. See below.
header: "Name", // Human-readable name for the column.
id: "name", // Unique ID for the column. Must be unique across all columns and used for sorting, filtering, etc.
plugins: { ... } // Extra plugin config for this column. See `svelte-headless-table` docs for more details.
},
```

If the `accessor` function returns a primitive value (e.g. a string), it is displayed in the cell as-is.
You can also provide a custom component to render in the cell instead. In this case, the `accessor` function can return any arbitrary value, which is accessible by the render function as `cell.value`.
The render function takes a Svelte component and a props object to pass to it. Consider this (simplified) example:

```typescript
import { Input } from "$lib/components/ui/input";

interface Person {
name: string;
age: number;
}

columnInitializers: ColumnInitializer<Person>[] = [
{
accessor: (row: Person) => row.name,
cell: (cell: Cell<Person>) => createRender(Input, { value: cell.value }),
header: "Name",
id: "name",
},
...
]

data = writable<Person[]>([
{ name: "John Doe", age: 30 },
...
]);

...
```

The equivalent of

```svelte
{#each data as person}
<Input value={person.name} />
{/each}
```

will be rendered in the "Name" column of the table.

## See also

- [svelte-headless-table](https://svelte-headless-table.bryanmylee.com/)
- [shadcn-svelte Data Table](https://www.shadcn-svelte.com/docs/components/data-table)
-->

<!-- Need `any` in some places due to the way the svelte-headless-table library is written -->
<!-- eslint-disable @typescript-eslint/no-explicit-any -->
<script lang="ts" generics="T, V = any">
import ActionsBtn from "./actions-btn.svelte";

import { Button } from "$lib/components/ui/button";
import * as Table from "$lib/components/ui/table";
import { cn } from "$lib/utils/ui";
Expand Down Expand Up @@ -52,14 +131,7 @@
cell: (cell) => createRender(ActionsBtn<T>, { actions, item: cell.value }),
header: "Actions",
id: "actions",
plugins: {
sort: {
disable: true,
},
tableFilter: {
disable: true,
},
},
plugins: { sort: { disable: true }, tableFilter: { disable: true } },
};
const colInits = [...columnInitializers, actionsCol];
const columns = table.createColumns(colInits.map((col) => table.column(col)));
Expand Down
9 changes: 9 additions & 0 deletions src/lib/components/data-table/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ import DataTableCore from "./data-table.svelte";

/* eslint-disable @typescript-eslint/no-explicit-any */

/** Map of human-readable action names to their implementations (functions that take the row item and do something with it) */
export type RowActions<T> = Map<string, (item: T) => void>;

/**
* Some boilerplate for column initializers to allow some basic type inference
* - `T` is the row type
* - `V` is the cell value type (returned by the cell's accessor function)
* - Middle value is the plugin config but defining it properly is complicated and probably not worth it
*/
export type ColumnInitializer<T, V = any> = DataColumnInitBase<T, any, V> &
DataColumnInitFnAndId<T, string, V>;

export { DataTableCore };
7 changes: 7 additions & 0 deletions src/lib/components/data-table/lib/column-hide-selector.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
<!--
Dropdown that allows the user to hide/show certain columns in a table.
Props:
- `flatColumns`: The table columns (see svelte-headless-table and the `data-table` component).
- `hideForId`: An object mapping column IDs to booleans (true = hidden). A column is hidden iff it is explicitly mapped to `true`.
-->

<script lang="ts">
// We need to allow explicit any here because of the way the svelte-headless-table library is written
/* eslint-disable @typescript-eslint/no-explicit-any */
Expand Down
15 changes: 15 additions & 0 deletions src/lib/components/data-table/lib/delete-dialog.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
<!--
# Delete Dialog Component

Invoked when the user clicks "delete" in a row's actions menu.
Displays a dialog asking the user to confirm deletion.
Disabling `settings.askDeleteConfirmation` will skip it and delete the item immediately.

Props:
- `selected`: The item to delete. Must be a `Base` object. (See $lib/model/core for details.)
- `open`: Whether the dialog is open or closed. Bindable; Defaults to `false` (closed).
- `extraDescription`: A string to show under the main description. Defaults to an empty string.
- `state`: The state containing the item. (See $lib/model for details.)
- `onDelete`: A callback that fires when the user clicks "delete". Takes no arguments. Defaults to a no-op.
-->

<script lang="ts">
import { Base } from "$lib/model/core";
import { state as GLOBAL_STATE } from "$lib/model";
Expand Down
4 changes: 4 additions & 0 deletions src/lib/components/data-table/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import ColumnHideSelector from "./column-hide-selector.svelte";
import TableHeader from "./table-header.svelte";

/**
* Displays a capacity range (e.g. (2, 5, "shifts") -> "2 - 5 shifts").
* TODO: We should probably move this to the utils folder.
*/
export function mkCapacity(min?: number, max?: number, title?: string) {
if (min) {
if (max) {
Expand Down
4 changes: 4 additions & 0 deletions src/lib/components/data-table/lib/table-header.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<!--
Helper component containing controls, displayed above a table.
-->

<script lang="ts">
let sticky = true;
export { sticky };
Expand Down
Loading