Merge remote-tracking branch 'origin/dev' into v2

This commit is contained in:
AdrianGonz97
2023-08-09 19:39:03 -04:00
57 changed files with 1962 additions and 117 deletions

View File

@@ -0,0 +1,5 @@
---
"@skeletonlabs/skeleton": patch
---
bugfix: `autocomplete` fixed reactive update when allow and deny lists are empty.

View File

@@ -0,0 +1,5 @@
---
"@skeletonlabs/skeleton": minor
---
feat: Added `tree-view` single/multi selection mode, Enabled `data-driven` for tree-view.

View File

@@ -0,0 +1,5 @@
---
"@skeletonlabs/skeleton": patch
---
bugfix: Fixed `.bg-hover-primary-token` color in dark mode.

View File

@@ -0,0 +1,6 @@
---
"@skeletonlabs/skeleton": minor
"skeleton.dev": patch
---
Added `regionLabel` prop to Radio Groups

View File

@@ -0,0 +1,5 @@
---
"@skeletonlabs/skeleton": patch
---
bugfix: InputChips updates bound value only once.

55
.github/workflows/release-v2.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Publish Prerelease Skeleton v2
on:
push:
branches:
- v2
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Build & Publish @latest Release
if: github.repository == 'skeletonlabs/skeleton'
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Use PNPM v8
uses: pnpm/action-setup@v2
with:
version: 8
- name: Use Node v18
uses: actions/setup-node@v3
with:
node-version: 18
# PNPM Store cache setup
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Create Release Pull Request or Publish
id: changesets
uses: changesets/action@v1
with:
commit: "chore(release): version package"
title: "chore(release): version package"
publish: pnpm ci:release
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -53,7 +53,7 @@ jobs:
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update tw-settings.json after publish
if: steps.changesets.outputs.published == 'true'
continue-on-error: true

View File

@@ -80,6 +80,7 @@
"Menlo",
"minima",
"mininal",
"Morty",
"Muertos",
"Nahua",
"Neue",

View File

@@ -208,8 +208,8 @@
@apply bg-surface-500/10 dark:bg-surface-500/20;
/* Border */
@apply border-0 border-b-2;
/* Blur */
@apply backdrop-blur;
/* Blur / high CPU usage on some browsers, see https://github.com/skeletonlabs/skeleton/issues/1805 */
/* @apply backdrop-blur; */
}
.variant-form-material[type='file'] {
@apply !py-1.5;

View File

@@ -10,7 +10,7 @@
/* === Logo Item (Child) === */
.logo-item {
@apply: flex-auto text-center space-x-4 shadow;
@apply flex-auto text-center space-x-4 shadow;
/* Center Contents */
@apply flex justify-center items-center space-x-4;
/* Background */

View File

@@ -18,7 +18,7 @@ export const backgrounds = (): CssClasses => {
// Hover
// Example: .bg-primary-hover-token
classes[`.bg-${n}-hover-token:hover`] = { 'background-color': `rgb(var(--color-${n}-500) / ${hoverAlpha})` };
classes[`.dark .bg-${n}-hover-token:hover`] = { 'background-color': `rgb(var(--color-${n}-200) / ${hoverAlpha})` };
classes[`.dark .bg-${n}-hover-token:hover`] = { 'background-color': `rgb(var(--color-${n}-500) / ${hoverAlpha})` };
// Active
// Example: .bg-primary-active-token

View File

@@ -1,5 +1,37 @@
# @skeletonlabs/skeleton
## 1.11.0
### Minor Changes
- chore: Added `separatorText` prop to Paginator to allow localization ([#1791](https://github.com/skeletonlabs/skeleton/pull/1791))
- feat: Added region props in `ListBox` and `ListBoxItem`. ([#1771](https://github.com/skeletonlabs/skeleton/pull/1771))
### Patch Changes
- bugfix: Removed `backdrop-blur` from `variant-form-material` because of high CPU usage on some browsers. ([#1807](https://github.com/skeletonlabs/skeleton/pull/1807))
- bugfix: fixed a minor typo in the Logo Cloud stylesheet ([#1769](https://github.com/skeletonlabs/skeleton/pull/1769))
- bugfix: resolved a scrolling issue when Modals exceeds the visible screen size ([#1695](https://github.com/skeletonlabs/skeleton/pull/1695))
## 1.10.0
### Minor Changes
- feat: AppRail has better support for padding, Tile and Anchors now include an `aspectRatio` property. ([#1727](https://github.com/skeletonlabs/skeleton/pull/1727))
- feat: A new `Tree View` component has been added. ([#1691](https://github.com/skeletonlabs/skeleton/pull/1691))
### Patch Changes
- bugfix: Fix accessibility warning in Ratings component due to applied `on:click` for non-interactive elements. ([#1739](https://github.com/skeletonlabs/skeleton/pull/1739))
- bugfix: Resolved an issue with Popups that caused them to be interactive when closed. ([#1721](https://github.com/skeletonlabs/skeleton/pull/1721))
- bugfix: Resolved an issue that prevented the Autocomplete `allowedlist` feature from working as expected. ([#1736](https://github.com/skeletonlabs/skeleton/pull/1736))
## 1.9.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@skeletonlabs/skeleton",
"version": "1.9.0",
"version": "1.11.0",
"description": "A SvelteKit component library.",
"author": "endigo9740 <chris@skeletonlabs.dev>",
"scripts": {

View File

@@ -1,3 +1,4 @@
const path = require('path');
const tailwindcss = require('tailwindcss');
const autoprefixer = require('autoprefixer');
@@ -5,7 +6,7 @@ const config = {
plugins: [
require('postcss-import'),
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
tailwindcss(),
tailwindcss(path.resolve(__dirname, './tailwind.config.js')),
//But others, like autoprefixer, need to run after,
autoprefixer
]

View File

@@ -6,8 +6,8 @@
"class",
"accent",
"active",
"aspectRatio",
"background",
"badge",
"bgBackdrop",
"bgDark",
"bgDrawer",
@@ -15,24 +15,13 @@
"blur",
"border",
"button",
"buttonAction",
"buttonBack",
"buttonClasses",
"buttonComplete",
"buttonDismiss",
"buttonNeutral",
"buttonNext",
"buttonPositive",
"buttonTextCancel",
"buttonTextConfirm",
"buttonTextFirst",
"buttonTextLast",
"buttonTextNext",
"buttonTextPrevious",
"buttonTextSubmit",
"caretClosed",
"caretOpen",
"chips",
"color",
"controlSeparator",
"controlVariant",
@@ -47,15 +36,18 @@
"gridColumns",
"height",
"hover",
"inactive",
"indent",
"justify",
"meter",
"padding",
"position",
"regionAnchor",
"regionBackdrop",
"regionBody",
"regionCaption",
"regionCaret",
"regionCell",
"regionChildren",
"regionCone",
"regionContent",
"regionControl",
@@ -63,7 +55,6 @@
"regionDrawer",
"regionFoot",
"regionFootCell",
"regionFooter",
"regionHead",
"regionHeadCell",
"regionHeader",
@@ -74,11 +65,14 @@
"regionLead",
"regionLegend",
"regionList",
"regionListItem",
"regionNavigation",
"regionPage",
"regionPanel",
"regionRowHeadline",
"regionRowMain",
"regionSummary",
"regionSymbol",
"regionTab",
"regionTrail",
"ring",

View File

@@ -31,32 +31,41 @@
export let regionTrail: CssClasses = '';
// Props (tiles/anchors)
/** Tile: Provide classes to set the tile hover background color. */
/** Provide classes to set the tile/anchor hover background color. */
export let hover: CssClasses = 'bg-primary-hover-token';
/** Tile: Provide classes to set the tile active tile background. */
/** Provide classes to set the tile/anchor active tile background. */
export let active: CssClasses = 'bg-primary-active-token';
/** Tile: Provide classes to set the tile vertical spacing. */
/** Provide classes to set the tile/anchor vertical spacing. */
export let spacing: CssClasses = 'space-y-1';
/** Provide classes to set the tile/anchor aspect ratio. */
export let aspectRatio: CssClasses = 'aspect-square';
// Context
setContext('active', active);
setContext('hover', hover);
setContext('spacing', spacing);
setContext('aspectRatio', aspectRatio);
// Base Classes
const cBase = 'grid grid-rows-[auto_1fr_auto] overflow-y-auto';
const cRegionLead = 'box-border';
const cRegionDefault = 'box-border';
const cRegionTrail = 'box-border';
// Reactive
$: classesBase = `${cBase} ${background} ${border} ${width} ${height} ${gap} ${$$props.class || ''}`;
$: classesRegionLead = `${cRegionLead} ${regionLead}`;
$: classesRegionDefault = `${cRegionDefault} ${regionDefault}`;
$: classesRegionTrail = `${cRegionTrail} ${regionTrail}`;
</script>
<!-- @component A vertical navigation rail component. -->
<div class="app-rail {classesBase}" data-testid="app-rail">
<!-- Slot: lead -->
<div class="app-bar-lead {regionLead}"><slot name="lead" /></div>
<div class="app-bar-lead {classesRegionLead}"><slot name="lead" /></div>
<!-- Slot: Default -->
<div class="app-bar-default {regionDefault}"><slot /></div>
<div class="app-bar-default {classesRegionDefault}"><slot /></div>
<!-- Slot: lead -->
<div class="app-bar-trail {regionTrail}"><slot name="trail" /></div>
<div class="app-bar-trail {classesRegionTrail}"><slot name="trail" /></div>
</div>

View File

@@ -23,17 +23,18 @@
export let hover: CssClasses = getContext('hover');
export let active: CssClasses = getContext('active');
export let spacing: CssClasses = getContext('spacing');
export let aspectRatio: CssClasses = getContext('aspectRatio');
// Classes
const cBase = 'unstyled';
const cWrapper = 'w-full aspect-square flex flex-col justify-center items-stretch text-center space-y-1';
const cWrapper = 'w-full flex flex-col justify-center items-stretch text-center space-y-1'; // CHRIS: keep `w-full` here please
const cLabel = 'font-bold text-xs';
// State
$: classActive = selected ? active : '';
// Reactive
$: classesBase = `${cBase} ${$$props.class || ''}`;
$: classesWrapper = `${cWrapper} ${hover} ${spacing} ${classActive}`;
$: classesBase = `${cBase} ${$$props.class || ''}`;
$: classesWrapper = `${cWrapper} ${aspectRatio} ${hover} ${spacing} ${classActive}`;
$: classesLead = `${regionLead}`;
$: classesLabel = `${cLabel} ${regionLabel}`;

View File

@@ -38,10 +38,12 @@
export let hover: CssClasses = getContext('hover');
export let active: CssClasses = getContext('active');
export let spacing: CssClasses = getContext('spacing');
export let width: CssClasses = getContext('width');
export let aspectRatio: CssClasses = getContext('aspectRatio');
// Classes
const cBase = 'cursor-pointer';
const cWrapper = 'w-full aspect-square flex flex-col justify-center items-stretch';
const cWrapper = 'flex flex-col justify-center items-stretch';
const cInterface = 'text-center';
const cLabel = 'font-bold text-xs';
@@ -51,8 +53,8 @@
// State
$: classActive = group === value ? active : '';
// Reactive
$: classesBase = `${cBase} ${$$props.class || ''}`;
$: classesWrapper = `${cWrapper} ${hover} ${classActive}`;
$: classesBase = `${cBase} ${$$props.class || ''}`;
$: classesWrapper = `${cWrapper} ${aspectRatio} ${width} ${hover} ${classActive}`;
$: classesInterface = `${cInterface} ${spacing}`;
$: classesLead = `${regionLead}`;
$: classesLabel = `${cLabel} ${regionLabel}`;

View File

@@ -90,32 +90,31 @@
// Local
$: listedOptions = options;
// Allowed Options
function filterByAllowed(): void {
function filterByAllowDeny(allowlist: unknown[], denylist: unknown[]) {
let _options = [...options];
// Allowed Options
if (allowlist.length) {
listedOptions = [...options].filter((option: AutocompleteOption) => allowlist.includes(option.value));
} else {
// IMPORTANT: required if the list goes from populated -> empty
listedOptions = [...options];
_options = _options.filter((option) => allowlist.includes(option.value));
}
}
// Denied Options
function filterByDenied(): void {
// Denied Options
if (denylist.length) {
const denySet = new Set(denylist);
listedOptions = [...options].filter((option: AutocompleteOption) => !denySet.has(option.value));
} else {
// IMPORTANT: required if the list goes from populated -> empty
listedOptions = [...options];
_options = _options.filter((option) => !denylist.includes(option.value));
}
// Reset options
if (!allowlist.length && !denylist.length) {
_options = options;
}
listedOptions = _options;
}
function filterOptions(): AutocompleteOption[] {
// Create a local copy of options
let _options = [...listedOptions];
// Filter options
_options = _options.filter((option: AutocompleteOption) => {
_options = _options.filter((option) => {
// Format the input search value
const inputFormatted = String(input).toLowerCase().trim();
// Format the option
@@ -132,8 +131,7 @@
}
// State
$: if (allowlist) filterByAllowed();
$: if (denylist) filterByDenied();
$: filterByAllowDeny(allowlist, denylist);
$: optionsFiltered = input ? filterOptions() : listedOptions;
$: sliceLimit = limit !== undefined ? limit : optionsFiltered.length;
// Reactive

View File

@@ -223,11 +223,6 @@
$: classesInterface = `${cInterface}`;
$: classesChipList = `${cChipList}`;
$: classesInputField = `${cInputField}`;
$: chipValues =
value?.map((val, i) => {
if (chipValues[i]?.val === val) return chipValues[i];
return { id: Math.random(), val: val };
}) || [];
</script>
<div class="input-chip {classesBase}" class:opacity-50={$$restProps.disabled}>

View File

@@ -22,6 +22,14 @@
/** Provide classes to set the listbox item padding styles. */
export let padding: CssClasses = 'px-4 py-2';
// Props (regions)
/** Provide arbitrary classes to style the lead region. */
export let regionLead: CssClasses = '';
/** Provide arbitrary classes to the default region. */
export let regionDefault: CssClasses = '';
/** Provide arbitrary classes to the trail region. */
export let regionTrail: CssClasses = '';
// Props (a11y)
/** Provide the ARIA labelledby value. */
export let labelledby = '';
@@ -32,6 +40,9 @@
setContext('active', active);
setContext('hover', hover);
setContext('padding', padding);
setContext('regionLead', regionLead);
setContext('regionDefault', regionDefault);
setContext('regionTrail', regionTrail);
// Classes
const cBase = '';

View File

@@ -24,6 +24,9 @@
export let active: CssClasses = getContext('active');
export let hover: CssClasses = getContext('hover');
export let padding: CssClasses = getContext('padding');
export let regionLead: CssClasses = getContext('regionLead');
export let regionDefault: CssClasses = getContext('regionDefault');
export let regionTrail: CssClasses = getContext('regionTrail');
// Classes
const cBase = 'cursor-pointer -outline-offset-[3px]';
@@ -85,11 +88,19 @@
}
}
// Base Classes
const cRegionLead = '';
const cRegionDefault = 'flex-1';
const cRegionTrail = '';
// Reactive
$: selected = multiple ? group.some((groupVal: unknown) => areDeeplyEqual(value, groupVal)) : areDeeplyEqual(group, value);
$: classesActive = selected ? active : hover;
$: classesBase = `${cBase} ${rounded} ${padding} ${classesActive} ${$$props.class ?? ''}`;
$: classesLabel = `${cLabel}`;
$: classesRegionLead = `${cRegionLead} ${regionLead}`;
$: classesRegionDefault = `${cRegionDefault} ${regionDefault}`;
$: classesRegionTrail = `${cRegionTrail} ${regionTrail}`;
</script>
<label>
@@ -116,11 +127,11 @@
<!-- <slot /> -->
<div class="listbox-label {classesLabel}">
<!-- Slot: Lead -->
{#if $$slots.lead}<div class="listbox-label-lead"><slot name="lead" /></div>{/if}
{#if $$slots.lead}<div class="listbox-label-lead {classesRegionLead}"><slot name="lead" /></div>{/if}
<!-- Slot: Default -->
<div class="listbox-label-content flex-1"><slot /></div>
<div class="listbox-label-content {classesRegionDefault}"><slot /></div>
<!-- Slot: Trail -->
{#if $$slots.trail}<div class="listbox-label-trail"><slot name="trail" /></div>{/if}
{#if $$slots.trail}<div class="listbox-label-trail {classesRegionTrail}"><slot name="trail" /></div>{/if}
</div>
</div>
</label>

View File

@@ -70,6 +70,8 @@
* @type {string}
*/
export let buttonTextLast: CssClasses = rightAngles;
/** Set the label for the pages separator. */
export let separatorText = 'of';
// Props (A11y)
/** Provide the ARIA label for the First page button. */
@@ -86,7 +88,7 @@
const cLabel = 'w-full md:w-auto';
// Local
let lastPage = Math.ceil(settings.size / settings.limit - 1);
let lastPage = Math.max(0, Math.ceil(settings.size / settings.limit - 1));
let controlPages: number[] = getNumerals();
function onChangeLength(): void {
@@ -151,11 +153,17 @@
return pages;
}
function updateSize(size: number) {
lastPage = Math.max(0, Math.ceil(size / settings.limit - 1));
controlPages = getNumerals();
}
// State
$: classesButtonActive = (page: number) => {
return page === settings.page ? `${active} pointer-events-none` : '';
};
$: maxNumerals, onChangeLength();
$: updateSize(settings.size);
// Reactive Classes
$: classesBase = `${cBase} ${justify} ${$$props.class ?? ''}`;
$: classesLabel = `${cLabel}`;
@@ -213,7 +221,7 @@
<!-- Details -->
<button type="button" class="{buttonClasses} pointer-events-none !text-sm">
{settings.page * settings.limit + 1}-{Math.min(settings.page * settings.limit + settings.limit, settings.size)}&nbsp;<span
class="opacity-50">of {settings.size}</span
class="opacity-50">{separatorText} {settings.size}</span
>
</button>
{:else}

View File

@@ -27,6 +27,8 @@
export let color: CssClasses = '';
/** Provide classes to set the highlighted SVG fill color. */
export let fill: CssClasses = '';
/** Provide classes for the label region. */
export let regionLabel: CssClasses = '';
// Props (a11y)
/** Provide the ARIA labelledby value. */
@@ -39,6 +41,7 @@
setContext('hover', hover);
setContext('color', color);
setContext('fill', fill);
setContext('regionLabel', regionLabel);
// Classes
const cBase = 'p-1';

View File

@@ -31,9 +31,11 @@
export let hover: CssClasses = getContext('hover');
export let color: CssClasses = getContext('color');
export let fill: CssClasses = getContext('fill');
export let regionLabel: CssClasses = getContext('regionLabel');
// Classes
const cBase = 'flex-auto text-base text-center cursor-pointer';
const cBase = 'flex-auto';
const cWrapper = 'text-base text-center cursor-pointer';
const cDisabled = 'opacity-50 cursor-not-allowed';
// Local
@@ -47,11 +49,13 @@
}
}
// Reactive
// State
$: checked = value === group;
$: classesActive = checked ? `${active} ${color} ${fill}` : hover;
$: classesDisabled = $$props.disabled ? cDisabled : '';
$: classesBase = `${cBase} ${padding} ${rounded} ${classesActive} ${classesDisabled} ${$$props.class ?? ''}`;
// Reactive
$: classsBase = `${cBase}`;
$: classesWrapper = `${cWrapper} ${padding} ${rounded} ${classesActive} ${classesDisabled} ${$$props.class ?? ''}`;
// RestProps
function prunedRestProps() {
@@ -60,10 +64,10 @@
}
</script>
<label>
<label class="radio-label {classsBase} {regionLabel}">
<!-- A11y attributes are not allowed on <label> -->
<div
class="radio-item {classesBase}"
class="radio-item {classesWrapper}"
data-testid="radio-item"
role="radio"
aria-checked={checked}

View File

@@ -46,36 +46,44 @@
});
}
function isFull(value: number, index: number) {
return Math.floor(value) >= index + 1;
}
function isHalf(value: number, index: number) {
return value === index + 0.5;
}
// Classes
const cBase = 'w-full flex';
// Reactive
$: elemInteractive = interactive ? 'button' : 'span';
$: attrInteractive = interactive ? { type: 'button' } : {};
$: classesBase = `${cBase} ${text} ${fill} ${justify} ${spacing} ${$$props.class ?? ''}`;
</script>
<div class="ratings {classesBase}" data-testid="rating-bar">
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
{#each Array(max) as _, i}
{#if Math.floor(value) >= i + 1}
<!-- FIXME: resolve a11y warnings -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<svelte:element this={elemInteractive} {...attrInteractive} class="rating-icon {regionIcon}" on:click={() => iconClick(i)}>
<slot name="full" />
</svelte:element>
{:else if value === i + 0.5}
<!-- FIXME: resolve a11y warnings -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<svelte:element this={elemInteractive} {...attrInteractive} class="rating-icon {regionIcon}" on:click={() => iconClick(i)}>
<slot name="half" />
</svelte:element>
{#each { length: max } as _, i}
{#if interactive}
<button class="rating-icon {regionIcon}" type="button" on:click={() => iconClick(i)}>
{#if isFull(value, i)}
<slot name="full" />
{:else if isHalf(value, i)}
<slot name="half" />
{:else}
<slot name="empty" />
{/if}
</button>
{:else}
<!-- FIXME: resolve a11y warnings -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<svelte:element this={elemInteractive} {...attrInteractive} class="rating-icon {regionIcon}" on:click={() => iconClick(i)}>
<slot name="empty" />
</svelte:element>
<span class="rating-icon {regionIcon}">
{#if isFull(value, i)}
<slot name="full" />
{:else if isHalf(value, i)}
<slot name="half" />
{:else}
<slot name="empty" />
{/if}
</span>
{/if}
{/each}
</div>

View File

@@ -0,0 +1,156 @@
<script lang="ts">
import { setContext } from 'svelte';
// Types
import type { CssClasses, TreeViewNode } from '../../index.js';
import TreeViewDataDrivenItem from './TreeViewDataDrivenItem.svelte';
// Props (parent)
/** Enable tree-view selection. */
export let selection = false;
/** Enable selection of multiple items. */
export let multiple = false;
/**
* Provide data-driven nodes.
* @type {TreeViewNode[]}
*/
export let nodes: TreeViewNode[] = [];
/** Provide classes to set the tree width. */
export let width: CssClasses = 'w-full';
/** Provide classes to set the vertical spacing between items. */
export let spacing: CssClasses = 'space-y-1';
// Props (children)
/** Set open by default on load. */
export let open = false;
/** Set the tree disabled state */
export let disabled = false;
/** Provide classes to set the tree item padding styles. */
export let padding: CssClasses = 'py-4 px-4';
/** Provide classes to set the tree children indentation */
export let indent: CssClasses = 'ml-4';
/** Provide classes to set the tree item hover styles. */
export let hover: CssClasses = 'hover:variant-soft';
/** Provide classes to set the tree item rounded styles. */
export let rounded: CssClasses = 'rounded-container-token';
// Props (symbols)
/** Set the rotation of the item caret in the open state. */
export let caretOpen: CssClasses = 'rotate-180';
/** Set the rotation of the item caret in the closed state. */
export let caretClosed: CssClasses = '';
/* Set the hyphen symbol opacity for non-expandable rows. */
export let hyphenOpacity: CssClasses = 'opacity-10';
// Props (regions)
/** Provide arbitrary classes to the tree item summary region. */
export let regionSummary: CssClasses = '';
/** Provide arbitrary classes to the symbol icon region. */
export let regionSymbol: CssClasses = '';
/** Provide arbitrary classes to the children region. */
export let regionChildren: CssClasses = '';
// Props A11y
/** Provide the ARIA labelledby value. */
export let labelledby = '';
// Functionality
/**
* expands all tree view items.
* @type {() => void}
*/
export function expandAll(): void {
const detailsElements = [...tree.querySelectorAll('details.tree-item')] as HTMLDetailsElement[];
detailsElements.forEach((details) => {
if (!details.open) {
const summary: HTMLElement | null = details.querySelector('summary.tree-item-summary');
if (summary) summary.click();
}
});
}
/**
* collapses all tree view items.
* @type {() => void}
*/
export function collapseAll(): void {
const detailsElements = [...tree.querySelectorAll('details.tree-item')] as HTMLDetailsElement[];
detailsElements.forEach((details) => {
if (details.open) {
const summary: HTMLElement | null = details.querySelector('summary.tree-item-summary');
if (summary) summary.click();
}
});
}
/**
* select all tree view items. Only available in Multiple selection mode.
* @type {() => void}
*/
export function selectAll(): void {
const detailsElements = [...tree.querySelectorAll('details.tree-item')] as HTMLDetailsElement[];
detailsElements.forEach((details) => {
const input: HTMLInputElement | null = details.querySelector('input[type="checkbox"].tree-item-checkbox');
if (!input) return;
if (!input.checked) {
// needs delay
setTimeout(() => {
input.click();
}, 5);
}
});
}
/**
* deselect all tree view items. Only available in Multiple selection mode.
* @type {() => void}
*/
export function deselectAll(): void {
const detailsElements = [...tree.querySelectorAll('details.tree-item')] as HTMLDetailsElement[];
detailsElements.forEach((details) => {
const input: HTMLInputElement | null = details.querySelector('input[type="checkbox"].tree-item-checkbox');
if (!input) return;
if (input.checked) {
// needs delay
setTimeout(() => {
input.click();
}, 5);
}
});
}
// Context API
setContext('open', open);
setContext('selection', selection);
setContext('multiple', multiple);
setContext('disabled', disabled);
setContext('padding', padding);
setContext('indent', indent);
setContext('hover', hover);
setContext('rounded', rounded);
setContext('caretOpen', caretOpen);
setContext('caretClosed', caretClosed);
setContext('hyphenOpacity', hyphenOpacity);
setContext('regionSummary', regionSummary);
setContext('regionSymbol', regionSymbol);
setContext('regionChildren', regionChildren);
// Reactive
$: classesBase = `${width} ${spacing} ${$$props.class ?? ''}`;
// Locals
let tree: HTMLDivElement;
</script>
<div
bind:this={tree}
class="tree {classesBase}"
data-testid="tree"
role="tree"
aria-multiselectable={multiple}
aria-label={labelledby}
aria-disabled={disabled}
>
{#if nodes && nodes.length > 0}
<TreeViewDataDrivenItem bind:nodes />
{:else}
<slot />
{/if}
</div>

View File

@@ -0,0 +1,31 @@
import { render } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import Tree from '$lib/components/TreeView/TreeView.svelte';
describe('Tree.svelte', () => {
it('Renders with minimal props', async () => {
const { getByTestId } = render(Tree);
expect(getByTestId('tree')).toBeTruthy();
});
it('Renders with all props', async () => {
const { getByTestId } = render(Tree, {
props: {
width: 'w-full',
spacing: 'space-y-1',
padding: 'py-4',
indent: 'ml-4',
hover: 'hover:variant-soft',
rounded: 'rounded-container-token',
caretOpen: 'rotate-180',
caretClosed: 'rotate-90',
regionSummary: 'bg-red-600',
regionCaret: 'bg-red-600',
regionChildren: 'bg-red-600',
labelledby: 'tree label'
}
});
expect(getByTestId('tree')).toBeTruthy();
});
});

View File

@@ -0,0 +1,93 @@
<script lang="ts">
/**
* This component is only in Data-driven tree-view to add children recursively.
*/
import { createEventDispatcher, getContext, onMount } from 'svelte';
import TreeViewDataDrivenItem from './TreeViewDataDrivenItem.svelte';
import TreeViewItem from './TreeViewItem.svelte';
import type { TreeViewNode } from './types.js';
// events
const dispatch = createEventDispatcher();
// this can't be passed using context, since we have to pass it to recursive children.
/** Provide data-driven nodes. */
export let nodes: TreeViewNode[] = [];
// Context API
/** Enable tree-view selection */
export let selection: boolean = getContext('selection');
/** Enable selection of multiple items. */
export let multiple: boolean = getContext('multiple');
// Locals
let group: unknown;
let name = '';
// Lifecycle
onMount(() => {
// random number as name
name = String(Math.random());
if (selection) {
group = multiple ? [] : '';
// manage group (checking) on initialization.
nodes.forEach((node) => {
if (!node.checked) return;
if (multiple) {
if (!Array.isArray(group)) return;
group.push(node.value);
} else {
group = node.value;
}
});
}
});
// Functionality
function onCheckChange() {
nodes.forEach((node) => {
if (multiple) {
if (!Array.isArray(group)) return;
node.checked = group.includes(node.value);
} else {
node.checked = group === node.value;
}
});
nodes = nodes;
// notify parent to update values. Important to recursively update parents.
dispatch('change');
}
export let parents: TreeViewItem[] = [];
let children: TreeViewItem[][] = [];
</script>
{#if nodes && nodes.length > 0}
{#each nodes as node, i}
<TreeViewItem
bind:this={parents[i]}
bind:open={node.open}
hideLead={!node.lead}
hideChildren={!node.children || node.children.length === 0}
bind:disabled={node.disabled}
bind:group
bind:name
bind:indeterminate={node.indeterminate}
bind:value={node.value}
bind:children={children[i]}
on:change={onCheckChange}
on:click
on:toggle
>
{@html node.content}
<svelte:fragment slot="lead">
{@html node.lead}
</svelte:fragment>
<svelte:fragment slot="children">
<TreeViewDataDrivenItem nodes={node.children} on:change={onCheckChange} bind:parents={children[i]} />
</svelte:fragment>
</TreeViewItem>
{/each}
{/if}

View File

@@ -0,0 +1,338 @@
<!-- To access props and events using reference -->
<svelte:options accessors />
<script lang="ts">
// Slots:
/**
* @slot {{}} lead - Allows for an optional leading element, such as an icon.
* @slot {{}} children - Provide TreeView item children.
*/
import { getContext, createEventDispatcher } from 'svelte';
// Types
import type { CssClasses, SvelteEvent, TreeViewItem } from '../../index.js';
// Props (state)
/**
* Set the radio group binding value.
* @type {unknown}
*/
export let group: unknown = undefined;
/**
* Set a unique name value for the input.
* @type {string | undefined}
*/
export let name: string | undefined = undefined;
/**
* Set the input's value.
* @type {unknown}
*/
export let value: unknown = undefined;
/**
* Provide children references to support relational checking.
* @type {TreeViewItem[]}
*/
export let children: TreeViewItem[] = [];
// Props (styles)
/** Provide classes to set the horizontal spacing. */
export let spacing: CssClasses = 'space-x-4';
// Context API
/** Set open by default on load. */
export let open: boolean = getContext('open');
/** Enable tree-view selection */
export let selection: boolean = getContext('selection');
/** Enable selection of multiple items. */
export let multiple: boolean = getContext('multiple');
/** Set the tree item disabled state */
export let disabled: boolean = getContext('disabled');
/** Set the check state to indeterminate(-). */
export let indeterminate = false;
// ---
/** Provide classes to set the tree item padding styles. */
export let padding: CssClasses = getContext('padding');
/** Provide classes to set the tree children indentation */
export let indent: CssClasses = getContext('indent');
/** Provide classes to set the tree item hover styles. */
export let hover: CssClasses = getContext('hover');
/** Provide classes to set the tree item rounded styles. */
export let rounded: CssClasses = getContext('rounded');
// ---
/** Set the rotation of the item caret in the open state. */
export let caretOpen: CssClasses = getContext('caretOpen');
/** Set the rotation of the item caret in the closed state. */
export let caretClosed: CssClasses = getContext('caretClosed');
/* Set the hyphen symbol opacity for non-expandable rows. */
export let hyphenOpacity: CssClasses = getContext('hyphenOpacity');
// ---
/** Provide arbitrary classes to the tree item summary region. */
export let regionSummary: CssClasses = getContext('regionSummary');
/** Provide arbitrary classes to the symbol icon region. */
export let regionSymbol: CssClasses = getContext('regionSymbol');
/** Provide arbitrary classes to the children region. */
export let regionChildren: CssClasses = getContext('regionChildren');
// Props (work-around)
/** Don't use this prop, workaround until svelte implements conditional slots */
export let hideLead = false;
/** Don't use this prop, workaround until svelte implements conditional slots */
export let hideChildren = false;
// Locals
let checked = false;
let treeItem: HTMLDetailsElement;
let childrenDiv: HTMLDivElement;
// Functionality
function onSummaryClick(event: MouseEvent) {
// prevent any action when disabled
if (disabled) event.preventDefault();
}
// Svelte Checkbox Bugfix
// GitHub: https://github.com/sveltejs/svelte/issues/2308
// REPL: https://svelte.dev/repl/de117399559f4e7e9e14e2fc9ab243cc?version=3.12.1
$: if (multiple) updateCheckbox(group);
$: if (multiple) updateGroup(checked);
function updateCheckbox(group: unknown) {
if (!Array.isArray(group)) return;
checked = group.indexOf(value) >= 0;
}
function updateGroup(checked: boolean) {
if (!Array.isArray(group)) return;
const index = group.indexOf(value);
if (checked) {
if (index < 0) {
group = [...group, value];
}
} else {
if (index >= 0) {
group.splice(index, 1);
group = group;
}
}
}
// called when a child's value is changed
function onChildValueChange(event: SvelteEvent<Event, HTMLInputElement>, child: TreeViewItem) {
if (multiple) {
// all groups must be arrays in multiple mode
if (!Array.isArray(group)) return;
const childrenValues = children.map((c) => c.value);
const index = group.indexOf(value);
// all children are checked => check parent
if (childrenValues.every((c) => Array.isArray(child.group) && child.group.includes(c))) {
if (index < 0) {
indeterminate = false;
group = [...group, value];
}
// not all children are checked => uncheck parent
} else {
if (index >= 0) {
group.splice(index, 1);
group = group;
}
// set parent to indeterminate if some of the children are checked
indeterminate = childrenValues.some((c) => Array.isArray(child.group) && child.group.includes(c));
}
// single selection mode
} else {
// child is checked, but parent isn't checked
if (event.currentTarget.checked && group !== value) {
// check parent
group = value;
}
}
}
// called every time the group's value changes
function onGroupValueChange(_group: unknown) {
// don't override children groups when parent is set to indeterminate
if (!children || children.length === 0 || indeterminate) return;
if (multiple) {
// group must by array in multiple mode
if (!Array.isArray(_group)) return;
const index = _group.indexOf(value);
const checkChild = (child: TreeViewItem) => {
if (!child || !Array.isArray(child.group)) return;
if (child.group.indexOf(child.value) < 0) {
// child.group = [...child.group, child.value] won't work here.
child.group.push(child.value);
child.group = child.group;
}
};
const uncheckChild = (child: TreeViewItem) => {
if (!child || !Array.isArray(child.group)) return;
const childIndex = child.group.indexOf(child.value);
if (childIndex >= 0) {
child.group.splice(childIndex, 1);
child.group = child.group;
}
};
// if parent is checked, check all children, else uncheck all children
children.forEach(index >= 0 ? checkChild : uncheckChild);
// single selection mode
} else {
// parent is not checked
if (_group !== value) {
// uncheck all children
children.forEach((child) => {
child.group = [];
});
}
}
}
$: onGroupValueChange(group);
// events
const dispatch = createEventDispatcher();
/** @event {{ open: boolean }} toggle - Fires on open or close. */
$: dispatch('toggle', { open: open });
// whenever children are changed, reassign on:change events.
$: children.forEach((child) => {
if (child) child.$on('change', (event) => onChildValueChange(event as SvelteEvent<Event, HTMLInputElement>, child));
});
// A11y Key Down Handler
function onKeyDown(event: SvelteEvent<KeyboardEvent, HTMLDivElement>): void {
function getRootTree(): HTMLDivElement | undefined {
let currentElement: HTMLElement | null = treeItem;
while (currentElement !== null) {
if (currentElement.classList.contains('tree')) return currentElement as HTMLDivElement;
currentElement = currentElement.parentElement;
}
return undefined;
}
let rootTree: HTMLDivElement | undefined = undefined;
let lastVisibleElement: HTMLElement | undefined | null = null;
switch (event.code) {
case 'ArrowRight':
if (!open) open = true;
else if ($$slots.children && !hideChildren) {
// focus on first child
const child = childrenDiv.querySelector('details>summary') as HTMLElement;
if (child) child.focus();
}
break;
case 'ArrowLeft':
if (open) open = false;
else {
// focus on parent
const parent = treeItem.parentElement?.parentElement;
if (parent && parent.tagName === 'DETAILS') (parent.querySelector('summary') as HTMLElement).focus();
}
break;
case 'Home':
event.preventDefault();
rootTree = getRootTree();
// focus on first node
if (rootTree) rootTree?.querySelector('summary')?.focus();
break;
case 'End':
event.preventDefault();
rootTree = getRootTree();
// focus on last node
if (rootTree) {
const detailsElements = rootTree?.querySelectorAll('details');
if (!detailsElements) return;
// start from last details
for (let i = detailsElements.length - 1; i >= 0; i--) {
const details = detailsElements[i];
// when details is on root level or the parent of details is open
if (details.parentElement?.classList?.contains('tree') || details.parentElement?.parentElement?.getAttribute('open') !== null) {
lastVisibleElement = details;
break;
// when details is not on root level but parent is not details
} else if (details.parentElement?.parentElement?.tagName !== 'details') {
lastVisibleElement = details.parentElement.parentElement;
break;
}
}
// focus on last visible node
if (lastVisibleElement) {
const summary = lastVisibleElement.querySelector('summary');
if (summary) summary.focus();
}
}
break;
}
}
// Classes
const cBase = '';
// [&::-webkit-details-marker]:hidden -> hide default arrow on webkit browsers
const cSummary = 'list-none [&::-webkit-details-marker]:hidden flex items-center cursor-pointer';
const cSymbol = 'fill-current w-3 text-center transition-transform duration-[200ms]';
const cChildren = '';
const cDisabled = 'opacity-50 !cursor-not-allowed';
// Reactive State Classes
$: classesCaretState = open && $$slots.children && !hideChildren ? caretOpen : caretClosed;
// Reactive Classes
$: classesDisabled = disabled ? cDisabled : '';
$: classesBase = `${cBase} ${$$props.class ?? ''}`;
$: classesSummary = `${cSummary} ${classesDisabled} ${spacing} ${rounded} ${padding} ${hover} ${regionSummary}`;
$: classesSymbol = `${cSymbol} ${classesCaret} ${regionSymbol}`;
$: classesCaret = `${classesCaretState}`;
$: classesHyphen = `${hyphenOpacity}`;
$: classesChildren = `${cChildren} ${indent} ${regionChildren}`;
</script>
<details bind:this={treeItem} bind:open class="tree-item {classesBase}" data-testid="tree-item" aria-disabled={disabled}>
<summary
class="tree-item-summary {classesSummary}"
role="treeitem"
aria-selected={selection ? checked : undefined}
aria-expanded={$$slots.children ? open : undefined}
on:click={onSummaryClick}
on:click
on:keydown={onKeyDown}
on:keydown
on:keyup
>
<!-- Symbol -->
<div class="tree-summary-symbol {classesSymbol}">
{#if $$slots.children && !hideChildren}
<!-- SVG Caret -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path
d="M201.4 374.6c12.5 12.5 32.8 12.5 45.3 0l160-160c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L224 306.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160z"
/>
</svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="w-3 {classesHyphen}">
<path d="M432 256c0 17.7-14.3 32-32 32L48 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l352 0c17.7 0 32 14.3 32 32z" />
</svg>
{/if}
</div>
<!-- Selection -->
{#if selection && name && group !== undefined}
{#if multiple}
<input class="checkbox tree-item-checkbox" type="checkbox" {name} {value} bind:checked bind:indeterminate on:click on:change />
{:else}
<input class="radio tree-item-radio" type="radio" bind:group {name} {value} on:click on:change />
{/if}
{/if}
<!-- Slot: Lead -->
{#if $$slots.lead && !hideLead}
<div class="tree-item-lead">
<slot name="lead" />
</div>
{/if}
<!-- Slot: Content -->
<div class="tree-item-content">
<slot />
</div>
</summary>
<div bind:this={childrenDiv} class="tree-item-children {classesChildren}" role="group">
<slot name="children" />
</div>
</details>

View File

@@ -0,0 +1,29 @@
import { render } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import TreeItem from '$lib/components/TreeView/TreeViewItem.svelte';
describe('TreeItem.svelte', () => {
it('Renders with minimal props', async () => {
const { getByTestId } = render(TreeItem);
expect(getByTestId('tree-item')).toBeTruthy();
});
it('Renders with all props', async () => {
const { getByTestId } = render(TreeItem, {
props: {
open: true,
padding: 'py-4',
indent: 'ml-4',
hover: 'hover:variant-soft',
rounded: 'rounded-container-token',
caretOpen: 'rotate-180',
caretClosed: 'rotate-90',
regionSummary: 'bg-red-600',
regionCaret: 'bg-red-600',
regionChildren: 'bg-red-600'
}
});
expect(getByTestId('tree-item')).toBeTruthy();
});
});

View File

@@ -0,0 +1,18 @@
export interface TreeViewNode {
/** Main content. accepts HTML. */
content: string;
/** Lead content. accepts HTML. */
lead?: string;
/** Set open by default on load. */
open?: boolean;
/** Set the tree disabled state. */
disabled?: boolean;
/** children nodes. */
children?: TreeViewNode[];
/** Set the input's value. */
value?: unknown;
/** input checked */
checked?: boolean;
/** input is set to indeterminate, only availabe in multiple selection mode. */
indeterminate?: boolean;
}

View File

@@ -11,6 +11,7 @@ export type { TableSource } from './components/Table/types.js';
export type { PaginationSettings } from './components/Paginator/types.js';
export type { PopupSettings } from './utilities/Popup/types.js';
export type { Transition, TransitionParams } from './internal/transitions.js';
export type { TreeViewNode } from './components/TreeView/types.js';
export type { CssClasses, SvelteEvent } from './types.js';
// Utilities ---
@@ -84,6 +85,9 @@ export { default as Table } from './components/Table/Table.svelte';
export { default as TabGroup } from './components/Tab/TabGroup.svelte';
export { default as Tab } from './components/Tab/Tab.svelte';
export { default as TabAnchor } from './components/Tab/TabAnchor.svelte';
export { default as TreeView } from './components/TreeView/TreeView.svelte';
export { default as TreeViewItem } from './components/TreeView/TreeViewItem.svelte';
export { default as TreeViewDataDrivenItem } from './components/TreeView/TreeViewDataDrivenItem.svelte';
// Utility Components
export { default as CodeBlock } from './utilities/CodeBlock/CodeBlock.svelte';
export { default as Modal } from './utilities/Modal/Modal.svelte';

View File

@@ -101,9 +101,9 @@
export let transitionOutParams: TransitionParams<TransitionOut> = { duration: 150, opacity: 0, x: 0, y: 100 };
// Base Styles
const cBackdrop = 'fixed top-0 left-0 right-0 bottom-0';
const cTransitionLayer = 'w-full h-full p-4 overflow-y-auto flex justify-center';
const cModal = 'block'; // max-h-full overflow-y-auto overflow-x-hidden
const cBackdrop = 'fixed top-0 left-0 right-0 bottom-0 overflow-y-auto';
const cTransitionLayer = 'w-full h-fit min-h-full p-4 overflow-y-auto flex justify-center';
const cModal = 'block overflow-y-auto'; // max-h-full overflow-y-auto overflow-x-hidden
const cModalImage = 'w-full h-auto';
// Local
@@ -225,8 +225,8 @@
data-testid="modal-backdrop"
on:mousedown={onBackdropInteractionBegin}
on:mouseup={onBackdropInteractionEnd}
on:touchstart
on:touchend
on:touchstart|passive
on:touchend|passive
transition:dynamicTransition|global={{ transition: fade, params: { duration: 150 }, enabled: transitions }}
use:focusTrap={true}
>

View File

@@ -104,6 +104,8 @@ export function popup(triggerNode: HTMLElement, args: PopupSettings) {
elemPopup.style.display = 'block';
elemPopup.style.opacity = '1';
elemPopup.style.pointerEvents = 'auto';
// enable popup interactions
elemPopup.removeAttribute('inert');
// Trigger Floating UI autoUpdate (open only)
// https://floating-ui.com/docs/autoUpdate
popupState.autoUpdateCleanup = autoUpdate(triggerNode, elemPopup, render);
@@ -121,7 +123,8 @@ export function popup(triggerNode: HTMLElement, args: PopupSettings) {
if (args.state) args.state({ state: popupState.open });
// Update the DOM
elemPopup.style.opacity = '0';
elemPopup.style.pointerEvents = 'none';
// disable popup interactions
elemPopup.setAttribute('inert', '');
// Cleanup Floating UI autoUpdate (close only)
if (popupState.autoUpdateCleanup) popupState.autoUpdateCleanup();
// Trigger callback

View File

@@ -1,5 +1,9 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/kit/vite';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/** @type {import('@sveltejs/kit').Config} */
const config = {
@@ -7,7 +11,11 @@ const config = {
// for more information about preprocessors
preprocess: [
vitePreprocess({
postcss: true
style: {
css: {
postcss: join(__dirname, 'postcss.config.cjs')
}
}
})
],
kit: {

View File

@@ -1,6 +1,7 @@
const path = require('path');
const tailwindcss = require('tailwindcss');
const autoprefixer = require('autoprefixer');
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
plugins: [tailwindcss(path.resolve(__dirname, './tailwind.config.js')), autoprefixer]
};

View File

@@ -124,6 +124,12 @@
<span>Blog</span>
</a>
</li>
<li>
<a href="https://store.skeleton.dev" target="_blank">
<span class="w-6 text-center"><i class="fa-solid fa-shopping-cart" /></span>
<span>Skeleton Store</span>
</a>
</li>
<hr class="!my-4" />
<li>
<a href="/elements/core">
@@ -164,6 +170,7 @@
<h6 class="h6">Mode</h6>
<LightSwitch />
</section>
<hr />
<nav class="list-nav p-4 -m-4 max-h-64 lg:max-h-[500px] overflow-y-auto">
<form action="/?/setTheme" method="POST" use:enhance={setTheme}>
<ul>
@@ -184,8 +191,9 @@
</ul>
</form>
</nav>
<hr />
<div>
<a class="btn variant-ghost-surface w-full" href="/docs/generator">Create a Theme</a>
<a class="btn variant-filled w-full" href="/docs/generator">Create a Theme</a>
</div>
</div>
<div class="arrow bg-surface-100-800-token" />

View File

@@ -42,6 +42,11 @@
<span>Home</span>
</AppRailAnchor>
<!-- prettier-ignore -->
<AppRailAnchor href="https://store.skeleton.dev" target="_blank" class="lg:hidden" on:click={() => { onClickAnchor() }}>
<svelte:fragment slot="lead"><i class="fa-solid fa-cart-shopping text-2xl" /></svelte:fragment>
<span>Store</span>
</AppRailAnchor>
<!-- prettier-ignore -->
<AppRailAnchor href="/blog" class="lg:hidden" on:click={() => { onClickAnchor() }}>
<svelte:fragment slot="lead"><i class="fa-solid fa-bullhorn text-2xl" /></svelte:fragment>
<span>Blog</span>

View File

@@ -37,6 +37,7 @@
roundedContainer: '8px',
borderBase: '1px'
});
resetColorOnInvalidHex();
// Local
let cssOutput = '';
@@ -44,6 +45,28 @@
let showThemeCSS = false;
let conReports: ContrastReport[] = getContrastReports();
// only called when initializing the component to avoid color errors.
// must be called before onMount.
function resetColorOnInvalidHex() {
const colorMapping = {
primary: '#0FBA81',
secondary: '#4F46E5',
tertiary: '#0EA5E9',
success: '#84cc16',
warning: '#EAB308',
error: '#D41976',
surface: '#495a8f'
};
$storeThemGenForm.colors.forEach((color: ColorSettings, i: number) => {
if (hexValueIsValid(color.hex)) return;
if (color.key in colorMapping) {
$storeThemGenForm.colors[i].hex = colorMapping[color.key];
}
});
}
function randomize(): void {
$storeThemGenForm.colors.forEach((_, i: number) => {
const randomHexCode = '#' + ((Math.random() * 0xffffff) << 0).toString(16).padStart(6, '0');

View File

@@ -223,7 +223,7 @@ export function getPassReport(textColor: string, backgroundColor: string): PassR
? 'is satisfactory for larger text'
: largeAA
? 'has poor contrast'
: 'fails contrast guidelines')
: 'fail contrast guidelines')
};
return {
textColor: _textColor,

View File

@@ -109,7 +109,8 @@ export const menuNavLinks: Record<string, Array<{ title: string; list: List }>>
{ href: '/components/slide-toggles', label: 'Slide Toggles', keywords: 'check, checkbox, toggle, input, form' },
{ href: '/components/steppers', label: 'Steppers', keywords: 'intro, onboard, onboarding, form, progress' },
{ href: '/components/tabs', label: 'Tabs', keywords: 'select, selection, panel' },
{ href: '/components/tables', label: 'Tables', keywords: 'data, entry' }
{ href: '/components/tables', label: 'Tables', keywords: 'data, entry' },
{ href: '/components/tree-views', label: 'Tree Views', keywords: 'tree, view, node', badge: 'Beta' }
]
}
],

View File

@@ -34,11 +34,6 @@
transitionIn: 'slide',
transitionOut: 'slide'
};
// Local
const loremIpsum =
// cspell:disable-next-line
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatum eligendi quibusdam odit, temporibus ullam ab enim expedita eum officia ipsum, laboriosam, nobis quasi laborum aspernatur reiciendis dignissimos optio sunt distinctio.';
</script>
<DocsShell {settings}>

View File

@@ -23,8 +23,8 @@
// aria: 'https://www.w3.org/WAI/ARIA/apg/',
components: [
{ label: 'AppRail', sveld: sveldAppRail },
{ label: 'AppRailTile', sveld: sveldAppRailTile, overrideProps: ['hover', 'active', 'spacing'] },
{ label: 'AppRailAnchor', sveld: sveldAppRailAnchor, overrideProps: ['hover', 'active', 'spacing'] }
{ label: 'AppRailTile', sveld: sveldAppRailTile, overrideProps: ['hover', 'active', 'spacing', 'width', 'aspectRatio'] },
{ label: 'AppRailAnchor', sveld: sveldAppRailAnchor, overrideProps: ['hover', 'active', 'spacing', 'width', 'aspectRatio'] }
],
restProps: 'AppRailTile | AppRailAnchor'
};
@@ -131,7 +131,7 @@
<CodeBlock
language="html"
code={`
<AppRailTile bind:group={currentTile} name="tile-1" value={0} title="tile-1>
<AppRailTile bind:group={currentTile} name="tile-1" value={0} title="tile-1">
<svelte:fragment slot="lead">(icon)</svelte:fragment>
<span>Tile 1</span>
</AppRailTile>

View File

@@ -3,7 +3,7 @@
import DocsShell from '$lib/layouts/DocsShell/DocsShell.svelte';
import { DocsFeature, type DocsShellSettings } from '$lib/layouts/DocsShell/types';
// Components
import { RadioGroup, RadioItem, CodeBlock } from '@skeletonlabs/skeleton';
import { CodeBlock, RadioGroup, RadioItem } from '@skeletonlabs/skeleton';
// Sveld
import sveldRadioGroup from '@skeletonlabs/skeleton/components/Radio/RadioGroup.svelte?raw&sveld';
import sveldRadioItem from '@skeletonlabs/skeleton/components/Radio/RadioItem.svelte?raw&sveld';
@@ -19,7 +19,11 @@
restProps: 'RadioItem input',
components: [
{ label: 'RadioGroup', sveld: sveldRadioGroup },
{ label: 'RadioItem', sveld: sveldRadioItem, overrideProps: ['padding', 'hover', 'accent', 'color', 'fill', 'rounded'] }
{
label: 'RadioItem',
sveld: sveldRadioItem,
overrideProps: ['padding', 'hover', 'accent', 'color', 'fill', 'rounded', 'regionLabel']
}
],
keyboard: [
['<kbd class="kbd">Tab</kbd>', 'Moves focus to the next focusable RadioItem.'],
@@ -40,6 +44,8 @@
<svelte:fragment slot="sandbox">
<DocsPreview>
<svelte:fragment slot="preview">
<!-- Use this to test full width sizing -->
<!-- <RadioGroup class="text-token w-full" display="flex"> -->
<RadioGroup class="text-token">
<RadioItem bind:group={justify} name="justify" value={0}>
<i class="fa-solid fa-align-left" />

View File

@@ -182,8 +182,8 @@
<CodeBlock
language="html"
code={`
<!-- French: Marcher 1, Marcher 2, ... -->
<Step stepTerm='Marcher'>...</Step>\n
<!-- French: Étape 1, Étape 2, ... -->
<Step stepTerm='Étape'>...</Step>\n
<!-- Spanish: Paso 1, Paso 2, ... -->
<Step stepTerm='Paso'>...</Step>
`}

View File

@@ -0,0 +1,897 @@
<script lang="ts">
import DocsShell from '$lib/layouts/DocsShell/DocsShell.svelte';
import { DocsFeature, type DocsShellSettings } from '$lib/layouts/DocsShell/types';
import DocsPreview from '$lib/components/DocsPreview/DocsPreview.svelte';
// Components
import { TreeView, TreeViewItem, type TreeViewNode } from '@skeletonlabs/skeleton';
// Utilities
import { CodeBlock } from '@skeletonlabs/skeleton';
// Sveld
import sveldTreeView from '@skeletonlabs/skeleton/components/TreeView/TreeView.svelte?raw&sveld';
import sveldTreeViewItem from '@skeletonlabs/skeleton/components/TreeView/TreeViewItem.svelte?raw&sveld';
// Docs Shell
const settings: DocsShellSettings = {
feature: DocsFeature.Component,
name: 'Tree Views',
description: 'Display information in a hierarchical structure using collapsible nodes.',
imports: ['TreeView', 'TreeViewItem', 'type TreeViewNode'],
source: 'components/TreeView',
aria: 'https://www.w3.org/WAI/ARIA/apg/patterns/treeview/',
components: [
{ label: 'TreeView', sveld: sveldTreeView },
{
label: 'TreeViewItem',
sveld: sveldTreeViewItem,
overrideProps: [
'open',
'selection',
'multiple',
'disabled',
'padding',
'indent',
'hover',
'rounded',
'caretOpen',
'hyphenOpacity',
'regionSummary',
'regionSymbol',
'regionChildren'
]
}
],
keyboard: [
['<kbd class="kbd">Tab</kbd>', "Focus the next tree-view item or it's input."],
['<kbd class="kbd">Shift + Tab</kbd> ', "Focus the previous tree-view item or it's input."],
['<kbd class="kbd">Right arrow</kbd>', 'Opens closed tree-view item or move focus to first child of open tree-view item.'],
['<kbd class="kbd">Left arrow</kbd>', 'Closes open tree-view item or move focus to parent of closed tree-view item.'],
['<kbd class="kbd">Home</kbd>', 'move focus to first tree-view item.'],
['<kbd class="kbd">End</kbd>', 'move focus to last tree-view item.']
]
};
// Locals
// single
let mediumSingle = 'books';
let booksSingle = 'Clean Code';
let relationalMediumSingle = 'books';
let relationalBooksSingle = 'Clean Code';
let childrenSingle: TreeViewItem[] = [];
// multi
let mediumMultiple = ['books', 'movies'];
let booksMultiple = ['Clean Code', 'The Art of Unix Programming'];
let relationalMediumMultiple = ['movies'];
let relationalBooksMultiple: string[] = [];
let childrenMultiple: TreeViewItem[] = [];
let expandTree: TreeView;
let selectTree: TreeView;
let selectMultiple: string[] = [];
let simpleDD: TreeViewNode[] = [
{
content: 'Books',
lead: '<i class="fa-solid fa-book-skull"></i>',
open: true,
children: [
{ content: 'Clean Code', value: 'Clean Code' },
{ content: 'The Clean Coder', value: 'The Clean Coder' },
{ content: 'The Art of Unix Programming', value: 'The Art of Unix Programming' }
],
value: 'books'
},
{
content: 'Movies',
lead: '<i class="fa-solid fa-film"></i>',
children: [
{ content: 'The Flash', value: 'The Flash' },
{ content: 'Guardians of the Galaxy', value: 'Guardians of the Galaxy' },
{ content: 'Black Panther', value: 'Black Panther' }
],
value: 'movies'
},
{
content: 'TV',
lead: '<i class="fa-solid fa-tv"></i>',
value: 'tv'
}
];
let singleDD: TreeViewNode[] = [
{
content: 'Books',
lead: '<i class="fa-solid fa-book-skull"></i>',
open: true,
checked: true,
children: [
{ content: 'Clean Code', value: 'Clean Code' },
{ content: 'The Clean Coder', value: 'The Clean Coder' },
{ content: 'The Art of Unix Programming', value: 'The Art of Unix Programming', checked: true }
],
value: 'books'
},
{
content: 'Movies',
lead: '<i class="fa-solid fa-film"></i>',
children: [
{ content: 'The Flash', value: 'The Flash' },
{ content: 'Guardians of the Galaxy', value: 'Guardians of the Galaxy' },
{ content: 'Black Panther', value: 'Black Panther' }
],
value: 'movies'
},
{
content: 'TV',
lead: '<i class="fa-solid fa-tv"></i>',
value: 'tv'
}
];
let multipleDD: TreeViewNode[] = [
{
content: 'Books',
lead: '<i class="fa-solid fa-book-skull"></i>',
open: true,
indeterminate: true,
children: [
{ content: 'Clean Code', value: 'Clean Code' },
{ content: 'The Clean Coder', value: 'The Clean Coder', checked: true },
{ content: 'The Art of Unix Programming', value: 'The Art of Unix Programming', checked: true }
],
value: 'books'
},
{
content: 'Movies',
lead: '<i class="fa-solid fa-film"></i>',
children: [
{ content: 'The Flash', value: 'The Flash' },
{ content: 'Guardians of the Galaxy', value: 'Guardians of the Galaxy' },
{ content: 'Black Panther', value: 'Black Panther' }
],
value: 'movies'
},
{
content: 'TV',
lead: '<i class="fa-solid fa-tv"></i>',
value: 'tv'
}
];
</script>
<DocsShell {settings}>
<!-- Slot: Sandbox -->
<svelte:fragment slot="sandbox">
<DocsPreview regionFooter="text-center">
<svelte:fragment slot="preview">
<div class="w-full max-w-[480px] card p-4 text-token">
<TreeView>
<TreeViewItem>
<p>Item 1</p>
<svelte:fragment slot="children">
<TreeViewItem>
<p>Child 1</p>
<svelte:fragment slot="children">
<TreeViewItem>
<p>Child of Child 1</p>
</TreeViewItem>
<TreeViewItem>
<p>Child of Child 2</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
<TreeViewItem>
<p>Child 2</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
<TreeViewItem>
<p>Item 2</p>
<svelte:fragment slot="children">
<TreeViewItem>
<p>Child</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
</TreeView>
</div>
</svelte:fragment>
<svelte:fragment slot="source">
<CodeBlock
language="html"
code={`
<TreeView>
<TreeViewItem>
(item 1)
<svelte:fragment slot="children">
<TreeViewItem>
(Child 1)
<svelte:fragment slot="children">
<TreeViewItem>
(Child of Child 1)
</TreeViewItem>
<TreeViewItem>
(Child of Child 2)
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
<TreeViewItem>
(Child 2)
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
<TreeViewItem>
(item 2)
</TreeViewItem>
</TreeView>
`}
/>
</svelte:fragment>
</DocsPreview>
</svelte:fragment>
<!-- Slot: Usage -->
<svelte:fragment slot="usage">
<!-- Icons -->
<section class="space-y-4">
<h2 class="h2">Icons</h2>
<DocsPreview background="neutral">
<svelte:fragment slot="preview">
<TreeView>
<TreeViewItem open>
<svelte:fragment slot="lead">
<i class="fa-solid fa-folder" />
</svelte:fragment>
<p>Folder</p>
<svelte:fragment slot="children">
<TreeViewItem>
<svelte:fragment slot="lead">
<i class="fa-solid fa-file" />
</svelte:fragment>
<p>File 1</p>
</TreeViewItem>
<TreeViewItem>
<svelte:fragment slot="lead">
<i class="fa-solid fa-file" />
</svelte:fragment>
<p>File 2</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
</TreeView>
</svelte:fragment>
<svelte:fragment slot="source">
<CodeBlock
language="html"
code={`
<TreeView>
<TreeViewItem>
<svelte:fragment slot="lead">(icon)</svelte:fragment>
(item 1)
</TreeViewItem>
</TreeView>
`}
/>
</svelte:fragment>
</DocsPreview>
</section>
<!-- Disbled -->
<section class="space-y-4">
<h2 class="h2">Disabled State</h2>
<DocsPreview background="neutral">
<svelte:fragment slot="preview">
<TreeView disabled>
<TreeViewItem open>
<svelte:fragment slot="lead">
<i class="fa-solid fa-book-skull" />
</svelte:fragment>
<p>Books</p>
<svelte:fragment slot="children">
<TreeViewItem>
<p>Clean Code</p>
</TreeViewItem>
<TreeViewItem>
<p>The Clean Coder</p>
</TreeViewItem>
<TreeViewItem>
<p>The Art of Unix Programming</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
<TreeViewItem>
<svelte:fragment slot="lead">
<i class="fa-solid fa-film" />
</svelte:fragment>
<p>Movies</p>
<svelte:fragment slot="children">
<TreeViewItem>
<p>Clean Code</p>
</TreeViewItem>
<TreeViewItem>
<p>The Clean Coder</p>
</TreeViewItem>
<TreeViewItem>
<p>The Art of Unix Programming</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
<TreeViewItem>
<svelte:fragment slot="lead">
<i class="fa-solid fa-tv" />
</svelte:fragment>
<p>TV</p>
</TreeViewItem>
</TreeView>
</svelte:fragment>
<svelte:fragment slot="source">
<p>Disable the entire tree view component</p>
<CodeBlock language="html" code={`<TreeView disabled></TreeView>`} />
<p>Disable individual item components.</p>
<CodeBlock
language="html"
code={`
<TreeView>
<TreeViewItem disabled></TreeViewItem>
<TreeViewItem open disabled></TreeViewItem>
</TreeView>
`}
/>
</svelte:fragment>
</DocsPreview>
</section>
<!-- Expand & Collapse -->
<section class="space-y-4">
<h2 class="h2">Expand & Collapse</h2>
<p>We can bind the tree view and trigger methods for expanding or collapsing all children at once.</p>
<DocsPreview background="neutral" regionFooter="flex justify-center gap-4">
<svelte:fragment slot="preview">
<TreeView bind:this={expandTree}>
<TreeViewItem>
<svelte:fragment slot="lead">
<i class="fa-solid fa-book-skull" />
</svelte:fragment>
<p>Books</p>
<svelte:fragment slot="children">
<TreeViewItem>
<p>Clean Code</p>
</TreeViewItem>
<TreeViewItem>
<p>The Clean Coder</p>
</TreeViewItem>
<TreeViewItem>
<p>The Art of Unix Programming</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
<TreeViewItem>
<svelte:fragment slot="lead">
<i class="fa-solid fa-film" />
</svelte:fragment>
<p>Movies</p>
<svelte:fragment slot="children">
<TreeViewItem>
<p>The Flash</p>
</TreeViewItem>
<TreeViewItem>
<p>Guardians of the Galaxy</p>
</TreeViewItem>
<TreeViewItem>
<p>Black Panther</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
<TreeViewItem>
<svelte:fragment slot="lead">
<i class="fa-solid fa-tv" />
</svelte:fragment>
<p>TV</p>
<svelte:fragment slot="children">
<TreeViewItem>
<p>The Simpsons</p>
</TreeViewItem>
<TreeViewItem>
<p>Rick and Morty</p>
</TreeViewItem>
<TreeViewItem>
<p>Family Guy</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
</TreeView>
</svelte:fragment>
<svelte:fragment slot="footer">
<button class="btn variant-filled" on:click={expandTree.expandAll}>Expand</button>
<button class="btn variant-filled" on:click={expandTree.collapseAll}>Collapse</button>
</svelte:fragment>
<svelte:fragment slot="source">
<CodeBlock
language="ts"
code={`
let myTreeView: TreeView;\n
myTreeView.expandAll();
myTreeView.collapseAll();
`}
/>
<CodeBlock
language="html"
code={`
<TreeView bind:this={myTreeView}></TreeView>
`}
/>
</svelte:fragment>
</DocsPreview>
</section>
<hr />
<!-- Selection -->
<section class="space-y-4">
<h2 class="h2">Selection</h2>
<p>Each tree view provides a number of selection options.</p>
<!-- Single -->
<h3 class="h3">Single</h3>
<p>When using single selection, our items are setup and treated as radio inputs.</p>
<DocsPreview background="neutral">
<svelte:fragment slot="preview">
<TreeView selection>
<TreeViewItem bind:group={mediumSingle} name="medium" value="books">
<svelte:fragment slot="lead">
<i class="fa-solid fa-book-skull" />
</svelte:fragment>
<p>Books</p>
<svelte:fragment slot="children">
<TreeViewItem bind:group={booksSingle} name="books" value="Clean Code">
<p>Clean Code</p>
</TreeViewItem>
<TreeViewItem bind:group={booksSingle} name="books" value="The Clean Coder">
<p>The Clean Coder</p>
</TreeViewItem>
<TreeViewItem bind:group={booksSingle} name="books" value="The Art of Unix Programming">
<p>The Art of Unix Programming</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
<TreeViewItem bind:group={mediumSingle} name="medium" value="movies">
<svelte:fragment slot="lead">
<i class="fa-solid fa-film" />
</svelte:fragment>
<p>Movies</p>
</TreeViewItem>
<TreeViewItem bind:group={mediumSingle} name="medium" value="tv">
<svelte:fragment slot="lead">
<i class="fa-solid fa-tv" />
</svelte:fragment>
<p>TV</p>
</TreeViewItem>
</TreeView>
</svelte:fragment>
<svelte:fragment slot="source">
<CodeBlock
language="ts"
code={`
let medium = 'books';
let books = 'Clean Code';
`}
/>
<CodeBlock
language="html"
code={`
<TreeView selection>
<TreeViewItem bind:group={medium} name="medium" value="books">
<svelte:fragment slot="lead">(icon)</svelte:fragment>
<p>Books</p>
<svelte:fragment slot="children">
<TreeViewItem bind:group={books} name="books" value="Clean Code">
<p>Clean Code</p>
</TreeViewItem>
<TreeViewItem bind:group={books} name="books" value="The Clean Coder">
<p>The Clean Coder</p>
</TreeViewItem>
<TreeViewItem bind:group={books} name="books" value="The Art of Unix Programming">
<p>The Art of Unix Programming</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
<!-- ... -->
</TreeView>
`}
/>
</svelte:fragment>
<svelte:fragment slot="footer">
<div class="flex justify-center items-center gap-4">
<span>Medium: <code class="code">{mediumSingle}</code></span>
<span>Books: <code class="code">{booksSingle}</code></span>
</div>
</svelte:fragment>
</DocsPreview>
<!-- Multiple -->
<h3 class="h3">Multiple</h3>
<p>When using multiple selection, our items are setup and treated as checkbox inputs.</p>
<DocsPreview background="neutral">
<svelte:fragment slot="preview">
<TreeView selection multiple>
<TreeViewItem bind:group={mediumMultiple} name="medium" value="books">
<svelte:fragment slot="lead">
<i class="fa-solid fa-book-skull" />
</svelte:fragment>
<p>Books</p>
<svelte:fragment slot="children">
<TreeViewItem bind:group={booksMultiple} name="books" value="Clean Code">
<p>Clean Code</p>
</TreeViewItem>
<TreeViewItem bind:group={booksMultiple} name="books" value="The Clean Coder">
<p>The Clean Coder</p>
</TreeViewItem>
<TreeViewItem bind:group={booksMultiple} name="books" value="The Art of Unix Programming">
<p>The Art of Unix Programming</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
<TreeViewItem bind:group={mediumMultiple} name="medium" value="movies">
<svelte:fragment slot="lead">
<i class="fa-solid fa-film" />
</svelte:fragment>
<p>Movies</p>
</TreeViewItem>
<TreeViewItem bind:group={mediumMultiple} name="medium" value="tv">
<svelte:fragment slot="lead">
<i class="fa-solid fa-tv" />
</svelte:fragment>
<p>TV</p>
</TreeViewItem>
</TreeView>
</svelte:fragment>
<svelte:fragment slot="source">
<CodeBlock
language="ts"
code={`
let medums = ['books', 'movies'];
let books = ['Clean Code', 'The Art of Unix Programming']
`}
/>
<CodeBlock
language="html"
code={`
<TreeView selection multiple>
<TreeViewItem bind:group={medums} name="medium" value="books">
<svelte:fragment slot="lead">(icon)</svelte:fragment>
<p>Books</p>
<svelte:fragment slot="children">
<TreeViewItem bind:group={books} name="books" value="Clean Code">
<p>Clean Code</p>
</TreeViewItem>
<TreeViewItem bind:group={books} name="books" value="The Clean Coder">
<p>The Clean Coder</p>
</TreeViewItem>
<TreeViewItem bind:group={books} name="books" value="The Art of Unix Programming">
<p>The Art of Unix Programming</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
<!-- ... -->
</TreeView>
`}
/>
</svelte:fragment>
<svelte:fragment slot="footer">
<div class="flex justify-center items-center gap-4">
<span>Mediums: <code class="code">{mediumMultiple.length ? mediumMultiple : 'None'}</code></span>
<span>Books: <code class="code">{booksMultiple.length ? booksMultiple : 'None'}</code></span>
</div>
</svelte:fragment>
</DocsPreview>
<!-- Relational -->
<h3 class="h3">Relational</h3>
<p>Use the <code class="code">children</code> prop to create a relational connection between parent and children.</p>
<DocsPreview background="neutral" regionFooter="text-center">
<svelte:fragment slot="preview">
<TreeView selection>
<TreeViewItem bind:group={relationalMediumSingle} name="r_medium" value="books" open children={childrenSingle}>
<svelte:fragment slot="lead">
<i class="fa-solid fa-book-skull" />
</svelte:fragment>
<p>Books</p>
<svelte:fragment slot="children">
<TreeViewItem bind:this={childrenSingle[0]} bind:group={relationalBooksSingle} name="r_books" value="Clean Code">
<p>Clean Code</p>
</TreeViewItem>
<TreeViewItem bind:this={childrenSingle[1]} bind:group={relationalBooksSingle} name="r_books" value="The Clean Coder">
<p>The Clean Coder</p>
</TreeViewItem>
<TreeViewItem
bind:this={childrenSingle[2]}
bind:group={relationalBooksSingle}
name="r_books"
value="The Art of Unix Programming"
>
<p>The Art of Unix Programming</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
<TreeViewItem bind:group={relationalMediumSingle} name="r_medium" value="movies">
<svelte:fragment slot="lead">
<i class="fa-solid fa-film" />
</svelte:fragment>
<p>Movies</p>
</TreeViewItem>
<TreeViewItem bind:group={relationalMediumSingle} name="r_medium" value="tv">
<svelte:fragment slot="lead">
<i class="fa-solid fa-tv" />
</svelte:fragment>
<p>TV</p>
</TreeViewItem>
</TreeView>
</svelte:fragment>
<svelte:fragment slot="source">
<CodeBlock
language="ts"
code={`
let medium = 'books';
let book = 'Clean Code';
let bookChildren: TreeViewItem[] = [];
`}
/>
<CodeBlock
language="html"
code={`
<TreeView selection>
<TreeViewItem bind:group={medium} name="medium" value="books" children={bookChildren}>
<svelte:fragment slot="lead">(icon)</svelte:fragment>
<p>Books</p>
<svelte:fragment slot="children">
<TreeViewItem bind:this={bookChildren[0]} bind:group={book} name="books" value="Clean Code">
<p>Clean Code</p>
</TreeViewItem>
<TreeViewItem bind:this={bookChildren[1]} bind:group={book} name="books" value="The Clean Coder">
<p>The Clean Coder</p>
</TreeViewItem>
<TreeViewItem bind:this={bookChildren[2]} bind:group={book} name="books" value="The Art of Unix Programming">
<p>The Art of Unix Programming</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
<!-- ... -->
</TreeView>
`}
/>
</svelte:fragment>
</DocsPreview>
<DocsPreview background="neutral" regionFooter="text-center">
<svelte:fragment slot="preview">
<TreeView selection multiple>
<TreeViewItem bind:group={relationalMediumMultiple} name="r_medium" value="books" open children={childrenMultiple}>
<svelte:fragment slot="lead">
<i class="fa-solid fa-book-skull" />
</svelte:fragment>
<p>Books</p>
<svelte:fragment slot="children">
<TreeViewItem bind:this={childrenMultiple[0]} bind:group={relationalBooksMultiple} name="r_books" value="Clean Code">
<p>Clean Code</p>
</TreeViewItem>
<TreeViewItem bind:this={childrenMultiple[1]} bind:group={relationalBooksMultiple} name="r_books" value="The Clean Coder">
<p>The Clean Coder</p>
</TreeViewItem>
<TreeViewItem
bind:this={childrenMultiple[2]}
bind:group={relationalBooksMultiple}
name="r_books"
value="The Art of Unix Programming"
>
<p>The Art of Unix Programming</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
<TreeViewItem bind:group={relationalMediumMultiple} name="r_medium" value="movies">
<svelte:fragment slot="lead">
<i class="fa-solid fa-film" />
</svelte:fragment>
<p>Movies</p>
</TreeViewItem>
<TreeViewItem bind:group={relationalMediumMultiple} name="r_medium" value="tv">
<svelte:fragment slot="lead">
<i class="fa-solid fa-tv" />
</svelte:fragment>
<p>TV</p>
</TreeViewItem>
</TreeView>
</svelte:fragment>
<svelte:fragment slot="source">
<CodeBlock
language="ts"
code={`
let mediums = ['movies'];
let books: string[] = [];
let booksChildren: TreeViewItem[] = [];
`}
/>
<CodeBlock
language="html"
code={`
<TreeView selection multiple>
<TreeViewItem bind:group={mediums} name="medium" value="books" children={children}>
<svelte:fragment slot="lead">(icon)</svelte:fragment>
<p>Books</p>
<svelte:fragment slot="children">
<TreeViewItem bind:this={booksChildren[0]} bind:group={books} name="books" value="Clean Code">
<p>Clean Code</p>
</TreeViewItem>
<TreeViewItem bind:this={booksChildren[1]} bind:group={books} name="books" value="The Clean Coder">
<p>The Clean Coder</p>
</TreeViewItem>
<TreeViewItem bind:this={booksChildren[2]} bind:group={books} name="books" value="The Art of Unix Programming">
<p>The Art of Unix Programming</p>
</TreeViewItem>
</svelte:fragment>
</TreeViewItem>
<!-- ... -->
</TreeView>
`}
/>
</svelte:fragment>
</DocsPreview>
<!-- Toggle All -->
<h3 class="h3">Toggle All</h3>
<p>By binding to the tree view component we can then toggle selection for all items.</p>
<blockquote class="blockquote">
Note: Available only when using <code class="code">multiple</code> selection mode.
</blockquote>
<DocsPreview background="neutral" regionFooter="flex justify-center gap-4">
<svelte:fragment slot="preview">
<TreeView selection multiple bind:this={selectTree}>
<TreeViewItem bind:group={selectMultiple} name="s_medium" value="books">
<svelte:fragment slot="lead">
<i class="fa-solid fa-book-skull" />
</svelte:fragment>
<p>Books</p>
</TreeViewItem>
<TreeViewItem bind:group={selectMultiple} name="s_medium" value="movies">
<svelte:fragment slot="lead">
<i class="fa-solid fa-film" />
</svelte:fragment>
<p>Movies</p>
</TreeViewItem>
<TreeViewItem bind:group={selectMultiple} name="s_medium" value="tv">
<svelte:fragment slot="lead">
<i class="fa-solid fa-tv" />
</svelte:fragment>
<p>TV</p>
</TreeViewItem>
</TreeView>
</svelte:fragment>
<svelte:fragment slot="footer">
<button class="btn variant-filled" on:click={selectTree.selectAll}> Select </button>
<button class="btn variant-filled" on:click={selectTree.deselectAll}> Deselect </button>
</svelte:fragment>
<svelte:fragment slot="source">
<CodeBlock
language="ts"
code={`
let tree: TreeView;\n
tree.selectAll();
tree.deselectAll();
`}
/>
<CodeBlock
language="html"
code={`
<TreeView bind:this={tree} selection multiple></TreeView>
`}
/>
</svelte:fragment>
</DocsPreview>
</section>
<hr />
<!-- Recursive Mode -->
<section class="space-y-4">
<h2 class="h2">Recursive Mode</h2>
<p>Tree views can be generated using a recursive data-driven method.</p>
<DocsPreview background="neutral" regionFooter="flex justify-center gap-4">
<svelte:fragment slot="preview">
<TreeView bind:nodes={simpleDD} />
</svelte:fragment>
<svelte:fragment slot="source">
<CodeBlock
language="ts"
code={`
let myTreeViewNodes: TreeViewNode[] = [
{
content: 'Books',
lead: '(icon)',
open: true,
children: [
{ content: 'Clean Code' },
{ content: 'The Clean Coder' },
{ content: 'The Art of Unix Programming' },
]
},
// ...
]
`}
/>
<CodeBlock
language="html"
code={`
<TreeView nodes={myTreeViewNodes}/>
`}
/>
</svelte:fragment>
</DocsPreview>
<!-- Single Selection -->
<h3 class="h3">Single Selection</h3>
<!-- prettier-ignore -->
<p>
Relational checking is automatically applied when generating your list in a recursive manner. Setting a child as <code class="code">checked</code> will not automatically affect the parent.
</p>
<DocsPreview background="neutral" regionFooter="flex justify-center gap-4">
<svelte:fragment slot="preview">
<TreeView bind:nodes={singleDD} selection />
</svelte:fragment>
<svelte:fragment slot="source">
<CodeBlock
language="ts"
code={`
let myTreeViewNodes: TreeViewNode[] = [
{
content: 'Books',
lead: '(icon)',
open: true,
checked: true,
children: [
{ content: 'Clean Code' },
{ content: 'The Clean Coder' },
{ content: 'The Art of Unix Programming', checked: true },
]
},
// ...
]
`}
/>
<CodeBlock
language="html"
code={`
<TreeView bind:nodes={myTreeViewNodes} selection/>
`}
/>
</svelte:fragment>
</DocsPreview>
<!-- Multiple Selection -->
<h3 class="h3">Multiple Selection</h3>
<p>Relational checking is automatically applied when generating your list in a recursive manner.</p>
<DocsPreview background="neutral" regionFooter="flex justify-center gap-4">
<svelte:fragment slot="preview">
<TreeView bind:nodes={multipleDD} selection multiple />
</svelte:fragment>
<svelte:fragment slot="source">
<CodeBlock
language="ts"
code={`
let myTreeViewNodes: TreeViewNode[] = [
{
content: 'Books',
lead: '(icon)',
open: true,
indeterminate: true,
children: [
{ content: 'Clean Code', checked: true },
{ content: 'The Clean Coder', checked: true },
{ content: 'The Art of Unix Programming' },
]
},
// ...
]
`}
/>
<CodeBlock
language="html"
code={`
<TreeView bind:nodes={myTreeViewNodes} selection multiple/>
`}
/>
</svelte:fragment>
</DocsPreview>
</section>
</svelte:fragment>
</DocsShell>

View File

@@ -81,7 +81,7 @@
<div>
<img class="h-auto max-w-full rounded-lg" src="${getImageLink({ id: 'YOErFW8AfkI', w: 128, h: 128 })}" alt="">
</div>
<section>
</section>
`}
/>
</svelte:fragment>

View File

@@ -100,6 +100,19 @@
and reusable via a Svelte writable store. Do not reimplement this component for each route page.
</p>
</aside>
<!-- Drawer Component -->
<section class="space-y-4">
<h2 class="h2">Drawer Component</h2>
<p>Implement a single instance of the drawer component in your app's root layout, above the App Shell (if present).</p>
<CodeBlock
language="html"
code={`
<Drawer>(contents)</Drawer>
<!-- <AppShell>...</AppShell> -->
`}
/>
</section>
<section class="space-y-4">
<h2 class="h2">Drawer Store</h2>
<p>Import this anywhere you wish to control the Drawer. Provides an interface to control the drawer component.</p>

View File

@@ -179,6 +179,18 @@ initializeStores();
and reusable via a Svelte writable store. Do not reimplement this component for each route page.
</p>
</aside>
<section class="space-y-4">
<h2 class="h2">Modal Component</h2>
<p>Implement a single instance of the modal component in your app's root layout, above the App Shell (if present).</p>
<CodeBlock
language="html"
code={`
<Modal />
<!-- <AppShell>...</AppShell> -->
`}
/>
</section>
<section class="space-y-4">
<h2 class="h2">Modal Store</h2>
<p>

View File

@@ -162,6 +162,19 @@
and reusable via a Svelte writable store. Do not reimplement this component for each route page.
</p>
</aside>
<!-- Toast Component -->
<section class="space-y-4">
<h2 class="h2">Toast Component</h2>
<p>Implement a single instance of the toast component in your app's root layout, above the App Shell (if present).</p>
<CodeBlock
language="html"
code={`
<Toast />
<!-- <AppShell>...</AppShell> -->
`}
/>
</section>
<!-- Toast Store -->
<section class="space-y-4">
<h2 class="h2">Toast Store</h2>

View File

@@ -19,6 +19,15 @@
</script>
<div>
<!-- Promo Banner -->
<!-- <div class="variant-filled-secondary p-4 flex justify-between items-center gap-4">
<div class="flex items-center gap-6">
<i class="fa-solid fa-cart-shopping text-xl" />
<p><strong>Skeleton Store</strong> now available! Get premium templates designed for Skeleton.</p>
</div>
<a class="btn variant-filled" href="https://store.skeleton.dev" target="_blank">Visit Store</a>
</div> -->
<!-- hero -->
<header id="hero" class="bg-surface-100-800-token hero-gradient">
<div class="section-container"><HomeHero /></div>

View File

@@ -12,7 +12,7 @@ export const load: PageLoad = async ({ fetch }) => {
return { contributors: getContributors() };
};
type Contributor = {
export type Contributor = {
login: string;
id: number;
node_id: string;

View File

@@ -24,7 +24,7 @@ export async function getBlogPost(slug: string): Promise<any> {
// Formatters ---
export function blogDateFormatter(date: string): string {
const options: any = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
const options: Intl.DateTimeFormatOptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
const d: Date = new Date(date);
return d.toLocaleDateString('en-US', options);
}

View File

@@ -1,7 +1,8 @@
<script lang="ts">
import DocsLogoLabs from '$lib/components/DocsLogos/DocsLogoLabs.svelte';
import { Avatar } from '@skeletonlabs/skeleton';
export let contributors: any[];
import type { Contributor } from '../+page';
export let contributors: Contributor[];
</script>
<div class="space-y-10">

View File

@@ -2,9 +2,23 @@
import { getImageLink } from '$lib/images';
import { TabGroup, Tab, ProgressBar } from '@skeletonlabs/skeleton';
type Theme = {
base: string;
header: string;
src: string;
img: string;
text1: string;
text2: string;
text3: string;
progressTrack: string;
progressMeter: string;
footer: string;
button: string;
};
// Local
let theme = 'simple';
const themeStyles: any = {
const themeStyles: Record<string, Theme> = {
simple: {
base: 'variant-soft-primary font-sans rounded-container-token',
header: 'bg-primary-500 p-4 grid grid-cols-3 gap-8 items-center rounded-tl-container-token rounded-tr-container-token',

View File

@@ -1,5 +1,9 @@
import adapter from '@sveltejs/adapter-vercel';
import { vitePreprocess } from '@sveltejs/kit/vite';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/** @type {import('@sveltejs/kit').Config} */
const config = {
@@ -7,7 +11,11 @@ const config = {
// for more information about preprocessors
preprocess: [
vitePreprocess({
postcss: true
style: {
css: {
postcss: join(__dirname, 'postcss.config.cjs')
}
}
})
],
kit: {