mirror of
https://github.com/LukeHagar/skeleton.git
synced 2025-12-06 12:47:44 +00:00
Merge remote-tracking branch 'origin/dev' into v2
This commit is contained in:
5
.changeset/clean-tables-boil.md
Normal file
5
.changeset/clean-tables-boil.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@skeletonlabs/skeleton": patch
|
||||
---
|
||||
|
||||
bugfix: `autocomplete` fixed reactive update when allow and deny lists are empty.
|
||||
5
.changeset/fresh-carrots-lick.md
Normal file
5
.changeset/fresh-carrots-lick.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@skeletonlabs/skeleton": minor
|
||||
---
|
||||
|
||||
feat: Added `tree-view` single/multi selection mode, Enabled `data-driven` for tree-view.
|
||||
5
.changeset/fresh-crews-drum.md
Normal file
5
.changeset/fresh-crews-drum.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@skeletonlabs/skeleton": patch
|
||||
---
|
||||
|
||||
bugfix: Fixed `.bg-hover-primary-token` color in dark mode.
|
||||
6
.changeset/shy-suits-provide.md
Normal file
6
.changeset/shy-suits-provide.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@skeletonlabs/skeleton": minor
|
||||
"skeleton.dev": patch
|
||||
---
|
||||
|
||||
Added `regionLabel` prop to Radio Groups
|
||||
5
.changeset/twenty-spoons-hope.md
Normal file
5
.changeset/twenty-spoons-hope.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@skeletonlabs/skeleton": patch
|
||||
---
|
||||
|
||||
bugfix: InputChips updates bound value only once.
|
||||
55
.github/workflows/release-v2.yml
vendored
Normal file
55
.github/workflows/release-v2.yml
vendored
Normal 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 }}
|
||||
@@ -80,6 +80,7 @@
|
||||
"Menlo",
|
||||
"minima",
|
||||
"mininal",
|
||||
"Morty",
|
||||
"Muertos",
|
||||
"Nahua",
|
||||
"Neue",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`;
|
||||
$: classesWrapper = `${cWrapper} ${aspectRatio} ${hover} ${spacing} ${classActive}`;
|
||||
$: classesLead = `${regionLead}`;
|
||||
$: classesLabel = `${cLabel} ${regionLabel}`;
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -52,7 +54,7 @@
|
||||
$: classActive = group === value ? active : '';
|
||||
// Reactive
|
||||
$: classesBase = `${cBase} ${$$props.class || ''}`;
|
||||
$: classesWrapper = `${cWrapper} ${hover} ${classActive}`;
|
||||
$: classesWrapper = `${cWrapper} ${aspectRatio} ${width} ${hover} ${classActive}`;
|
||||
$: classesInterface = `${cInterface} ${spacing}`;
|
||||
$: classesLead = `${regionLead}`;
|
||||
$: classesLabel = `${cLabel} ${regionLabel}`;
|
||||
|
||||
@@ -90,32 +90,31 @@
|
||||
// Local
|
||||
$: listedOptions = options;
|
||||
|
||||
function filterByAllowDeny(allowlist: unknown[], denylist: unknown[]) {
|
||||
let _options = [...options];
|
||||
// Allowed Options
|
||||
function filterByAllowed(): void {
|
||||
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 {
|
||||
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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)} <span
|
||||
class="opacity-50">of {settings.size}</span
|
||||
class="opacity-50">{separatorText} {settings.size}</span
|
||||
>
|
||||
</button>
|
||||
{:else}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)}>
|
||||
{#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" />
|
||||
</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)}>
|
||||
{:else if isHalf(value, i)}
|
||||
<slot name="half" />
|
||||
</svelte:element>
|
||||
{: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>
|
||||
{/if}
|
||||
</button>
|
||||
{:else}
|
||||
<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>
|
||||
|
||||
156
packages/skeleton/src/lib/components/TreeView/TreeView.svelte
Normal file
156
packages/skeleton/src/lib/components/TreeView/TreeView.svelte
Normal 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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
18
packages/skeleton/src/lib/components/TreeView/types.ts
Normal file
18
packages/skeleton/src/lib/components/TreeView/types.ts
Normal 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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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]
|
||||
};
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
`}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user