pretty selects everywhere

This commit is contained in:
tglide
2023-10-12 12:44:56 +01:00
parent 776bcc9bee
commit f2073ee532
7 changed files with 670 additions and 602 deletions

View File

@@ -28,7 +28,8 @@
}, },
title: 'Auth', title: 'Auth',
subtitle: 'Secure login for all users', subtitle: 'Secure login for all users',
description: 'Authenticate users securely with multiple login methods like Email/Password, SMS, OAuth, Annoymous, Magic URLs and more.', description:
'Authenticate users securely with multiple login methods like Email/Password, SMS, OAuth, Annoymous, Magic URLs and more.',
features: [ features: [
'30+ login methods', '30+ login methods',
'Support for teams, roles and user labels', 'Support for teams, roles and user labels',
@@ -44,14 +45,13 @@
}, },
title: 'Databases', title: 'Databases',
subtitle: 'Store, query and manage data', subtitle: 'Store, query and manage data',
description: description: 'Scalable and robust database backed by your favorite technologies.',
'Scalable and robust database backed by your favorite technologies.',
features: [ features: [
'Never paused', 'Never paused',
'Fast in-memory caching', 'Fast in-memory caching',
'Advanced permission models', 'Advanced permission models',
'Custom data validation', 'Custom data validation',
'Relationships support', 'Relationships support'
], ],
shot: './images/products/databases.png' shot: './images/products/databases.png'
}, },
@@ -78,7 +78,8 @@
}, },
title: 'Storage', title: 'Storage',
subtitle: 'Upload and manage files', subtitle: 'Upload and manage files',
description: 'Securely store files with advanced compression, encryption and image transformations.', description:
'Securely store files with advanced compression, encryption and image transformations.',
features: [ features: [
'File encryption at rest and transit', 'File encryption at rest and transit',
'Built-in image transformation capabilities', 'Built-in image transformation capabilities',
@@ -97,7 +98,7 @@
features: [ features: [
'Unlimited subscriptions', 'Unlimited subscriptions',
'Built-in permission management', 'Built-in permission management',
'Support for DBs, Auth, Storage & Functions', 'Support for DBs, Auth, Storage & Functions'
], ],
shot: './images/products/realtime.png' shot: './images/products/realtime.png'
} }
@@ -123,6 +124,7 @@
import { Realtime, realtimeController } from './realtime'; import { Realtime, realtimeController } from './realtime';
import { postController } from './post'; import { postController } from './post';
import Post from './post/post.svelte'; import Post from './post/post.svelte';
import { anyify } from '$lib/utils/anyify';
/* Basic Animation setup */ /* Basic Animation setup */
let scrollInfo = { let scrollInfo = {
@@ -180,8 +182,6 @@
} }
}); });
})(); })();
const anyify = (x: unknown) => x as any;
</script> </script>
<div <div
@@ -202,9 +202,14 @@
in:fly={{ duration: 250, delay: 250, y: -300 }} in:fly={{ duration: 250, delay: 250, y: -300 }}
> >
{#if scrollInfo.percentage > -0.1} {#if scrollInfo.percentage > -0.1}
<span class="aw-badges aw-eyebrow" transition:slide={{ axis: 'x' }}>Products_</span> <span class="aw-badges aw-eyebrow" transition:slide={{ axis: 'x' }}
>Products_</span
>
<h2 class="aw-display aw-u-color-text-primary " transition:fly={{ y: 16, delay: 250 }}> <h2
class="aw-display aw-u-color-text-primary"
transition:fly={{ y: 16, delay: 250 }}
>
Your backend, minus the hassle Your backend, minus the hassle
</h2> </h2>
<p <p
@@ -214,7 +219,8 @@
delay: 400 delay: 400
}} }}
> >
Build secure and scalable applications with less code. Add authentication, databases, storage, and more using Appwrite's development platform. Build secure and scalable applications with less code. Add authentication,
databases, storage, and more using Appwrite's development platform.
</p> </p>
{/if} {/if}
</div> </div>
@@ -226,7 +232,9 @@
data-active={scrollInfo.percentage > 0.075 ? '' : undefined} data-active={scrollInfo.percentage > 0.075 ? '' : undefined}
> >
<div class="text" id="pd-{$elId}"> <div class="text" id="pd-{$elId}">
<ScrollIndicator percentage={toScale(scrollInfo.percentage, animScale, [0, 1])} /> <ScrollIndicator
percentage={toScale(scrollInfo.percentage, animScale, [0, 1])}
/>
<ul class="descriptions"> <ul class="descriptions">
{#each products as product} {#each products as product}
{@const copy = infos[product]} {@const copy = infos[product]}
@@ -235,7 +243,12 @@
{#if copy} {#if copy}
<li data-active={isActive ? '' : undefined}> <li data-active={isActive ? '' : undefined}>
<h3> <h3>
<img src={isActive ? copy.icon.active : copy.icon.inactive} alt="" width="32" height="32" /> <img
src={isActive ? copy.icon.active : copy.icon.inactive}
alt=""
width="32"
height="32"
/>
<span class="aw-label">{copy.title}</span> <span class="aw-label">{copy.title}</span>
</h3> </h3>
{#if isActive} {#if isActive}

View File

@@ -9,37 +9,49 @@
<script lang="ts"> <script lang="ts">
import { createSelect, melt, type CreateSelectProps } from '@melt-ui/svelte'; import { createSelect, melt, type CreateSelectProps } from '@melt-ui/svelte';
import { fly } from 'svelte/transition'; import { createEventDispatcher } from 'svelte';
import { fly, type FlyParams } from 'svelte/transition';
export let options: Array<SelectOption>; export let options: Array<SelectOption>;
export let nativeMobile = false; export let nativeMobile = false;
export let selected: unknown | undefined = undefined; export let value: unknown | undefined = undefined;
export let onSelectedChange: CreateSelectProps['onSelectedChange'] = undefined; export let onSelectedChange: CreateSelectProps['onSelectedChange'] = undefined;
// TODO: This id currently gets overriden by Melt. We should either use the label el, or
// allow passing down ids in Melt.
export let id: string | undefined = undefined;
export let preventScroll = false;
export let placement: NonNullable<CreateSelectProps['positioning']>['placement'] = 'bottom';
const dispatch = createEventDispatcher<{
change: unknown;
}>();
const { const {
elements: { trigger, menu, option: optionEl, group: groupEl, groupLabel }, elements: { trigger, menu, option: optionEl, group: groupEl, groupLabel },
states: { open, selected: localSelected, selectedLabel } states: { open, selected, selectedLabel }
} = createSelect<unknown>({ } = createSelect<unknown>({
preventScroll: false, preventScroll,
positioning: { positioning: {
sameWidth: true, sameWidth: true,
fitViewport: true fitViewport: true,
placement
}, },
forceVisible: true, forceVisible: true,
onSelectedChange({ curr, next }) { onSelectedChange({ curr, next }) {
if (onSelectedChange) { if (onSelectedChange) {
onSelectedChange({ curr, next }); onSelectedChange({ curr, next });
} }
selected = next?.value; value = next?.value;
dispatch('change', next?.value);
return next; return next;
} }
}); });
$: selectedOption = options.find((o) => o.value === selected); $: selectedOption = options.find((o) => o.value === value);
$: if (selectedOption) { $: if (selectedOption) {
localSelected.set(selectedOption); selected.set(selectedOption);
} }
const DEFAULT_GROUP = 'default'; const DEFAULT_GROUP = 'default';
@@ -59,10 +71,16 @@
return Object.entries(groups).map(([label, options]) => ({ label, options })); return Object.entries(groups).map(([label, options]) => ({ label, options }));
})(); })();
$: flyParams = {
duration: 150,
y: placement === 'top' ? 4 : -4
} as FlyParams;
</script> </script>
<button <button
class="aw-select is-colored" class="aw-select is-colored"
{id}
class:aw-is-not-mobile={nativeMobile} class:aw-is-not-mobile={nativeMobile}
use:melt={$trigger} use:melt={$trigger}
aria-label="Select theme" aria-label="Select theme"
@@ -82,14 +100,25 @@
class:aw-is-not-mobile={nativeMobile} class:aw-is-not-mobile={nativeMobile}
style:z-index={10000} style:z-index={10000}
use:melt={$menu} use:melt={$menu}
transition:fly={{ y: 4, duration: 150 }} transition:fly={flyParams}
> >
{#each groups as group} {#each groups as group}
{@const isDefault = group.label === DEFAULT_GROUP} {@const isDefault = group.label === DEFAULT_GROUP}
<div class="aw-select-group" use:melt={$groupEl(group.label)}> {#if isDefault}
{#if !isDefault} {#each options as option}
<span class="aw-select-group-label" use:melt={$groupLabel}>{group.label}</span> <button class="aw-select-option" use:melt={$optionEl(option)}>
{#if option.icon}
<span class={option.icon} aria-hidden="true" />
{/if} {/if}
<span style:text-transform="capitalize">{option.label}</span>
</button>
{/each}
{:else}
<div class="aw-select-group" use:melt={$groupEl(group.label)}>
<span class="aw-select-group-label" use:melt={$groupLabel(group.label)}>
{group.label}
</span>
{#each options as option} {#each options as option}
<button class="aw-select-option" use:melt={$optionEl(option)}> <button class="aw-select-option" use:melt={$optionEl(option)}>
{#if option.icon} {#if option.icon}
@@ -99,6 +128,7 @@
</button> </button>
{/each} {/each}
</div> </div>
{/if}
{/each} {/each}
</div> </div>
{/if} {/if}
@@ -110,19 +140,19 @@
{#if selectedOption?.icon} {#if selectedOption?.icon}
<span class={selectedOption.icon} aria-hidden="true" /> <span class={selectedOption.icon} aria-hidden="true" />
{/if} {/if}
<select bind:value={selected}> <select {id} bind:value>
{#each groups as group} {#each groups as group}
{@const isDefault = group.label === DEFAULT_GROUP} {@const isDefault = group.label === DEFAULT_GROUP}
{#if isDefault} {#if isDefault}
{#each options as option} {#each options as option}
<option value={option.value} selected={option.value === selected}> <option value={option.value} selected={option.value === value}>
{option.label} {option.label}
</option> </option>
{/each} {/each}
{:else} {:else}
<optgroup label={isDefault ? undefined : group.label}> <optgroup label={isDefault ? undefined : group.label}>
{#each options as option} {#each options as option}
<option value={option.value} selected={option.value === selected}> <option value={option.value} selected={option.value === value}>
{option.label} {option.label}
</option> </option>
{/each} {/each}
@@ -132,3 +162,9 @@
</select> </select>
<span class="icon-cheveron-down" aria-hidden="true" /> <span class="icon-cheveron-down" aria-hidden="true" />
</div> </div>
<style lang="scss">
.aw-select {
min-width: var(--min-width, var(--p-select-min-width));
}
</style>

View File

@@ -22,4 +22,4 @@
]; ];
</script> </script>
<Select {options} bind:selected={$currentTheme} nativeMobile /> <Select {options} bind:value={$currentTheme} placement="top" />

1
src/lib/utils/anyify.ts Normal file
View File

@@ -0,0 +1 @@
export const anyify = (x: unknown) => x as any;

View File

@@ -2,15 +2,13 @@ import { writable } from 'svelte/store';
import type { Language } from './code'; import type { Language } from './code';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
export type Version = '1.4.x' | '1.3.x' | '1.2.x' | '1.1.x' | '1.0.x' | '0.15.x' | 'cloud'; const allVersions = ['1.4.x', '1.3.x', '1.2.x', '1.1.x', '1.0.x', '0.15.x', 'cloud'] as const;
export const versions: Readonly<Array<Omit<Version, 'cloud'>>> = [
'1.4.x', export type Version = (typeof allVersions)[number];
'1.3.x',
'1.2.x', export const versions: Readonly<Array<Omit<Version, 'cloud'>>> = allVersions.filter(
'1.1.x', (v) => v !== 'cloud'
'1.0.x', );
'0.15.x'
] as const;
export enum Service { export enum Service {
Account = 'account', Account = 'account',

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { MainFooter } from '$lib/components'; import { MainFooter, Select } from '$lib/components';
import { DEFAULT_HOST } from '$lib/utils/metadata'; import { DEFAULT_HOST } from '$lib/utils/metadata';
import { layoutState, toggleReferences } from '$lib/layouts/Docs.svelte'; import { layoutState, toggleReferences } from '$lib/layouts/Docs.svelte';
import { parse } from '$lib/utils/markdown'; import { parse } from '$lib/utils/markdown';
@@ -19,6 +19,7 @@
import { API_REFERENCE_TITLE_SUFFIX } from '$routes/titles.js'; import { API_REFERENCE_TITLE_SUFFIX } from '$routes/titles.js';
import { getContext, onMount, setContext } from 'svelte'; import { getContext, onMount, setContext } from 'svelte';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { anyify } from '$lib/utils/anyify.js';
export let data; export let data;
@@ -40,18 +41,18 @@
} }
}); });
function selectPlatform(event: Event & { currentTarget: EventTarget & HTMLSelectElement }) { function selectPlatform(event: CustomEvent<unknown>) {
const { version, service } = $page.params; const { version, service } = $page.params;
const platform = event.currentTarget.value as Platform; const platform = event.detail as Platform;
preferredPlatform.set(platform); preferredPlatform.set(platform);
goto(`/docs/references/${version}/${event.currentTarget.value}/${service}`, { goto(`/docs/references/${version}/${platform}/${service}`, {
noScroll: true noScroll: true
}); });
} }
function selectVersion(event: Event & { currentTarget: EventTarget & HTMLSelectElement }) { function selectVersion(event: CustomEvent<unknown>) {
const { platform, service } = $page.params; const { platform, service } = $page.params;
const version = event.currentTarget.value as Version; const version = event.detail as Version;
preferredVersion.set(version); preferredVersion.set(version);
goto(`/docs/references/${version}/${platform}/${service}`, { goto(`/docs/references/${version}/${platform}/${service}`, {
noScroll: true noScroll: true
@@ -99,37 +100,44 @@
<div class="u-flex u-gap-24 aw-u-color-text-primary"> <div class="u-flex u-gap-24 aw-u-color-text-primary">
<div class="u-flex u-cross-center u-gap-8"> <div class="u-flex u-cross-center u-gap-8">
<label class="u-small is-not-mobile" for="platform">Platform</label> <label class="u-small is-not-mobile" for="platform">Platform</label>
<div class="aw-select is-colored"> <Select
<select id="platform" on:change={selectPlatform} value={platform}> --min-width="10rem"
<optgroup label="Client"> id="platform"
{#each Object.values(Platform).filter( (p) => p.startsWith('client-') ) as platform} value={platform}
<option value={platform}>{platformMap[platform]}</option> on:change={selectPlatform}
{/each} options={[
</optgroup> ...Object.values(Platform)
<optgroup label="Server"> .filter((p) => p.startsWith('client-'))
{#each Object.values(Platform).filter( (p) => p.startsWith('server-') ) as platform} .map((p) => ({
<option value={platform}>{platformMap[platform]}</option> value: p,
{/each} label: platformMap[p],
</optgroup> group: 'Client'
</select> })),
<span class="icon-cheveron-down" aria-hidden="true" /> ...Object.values(Platform)
</div> .filter((p) => p.startsWith('server-'))
.map((p) => ({
value: p,
label: platformMap[p],
group: 'Server'
}))
]}
nativeMobile
/>
</div> </div>
<div class="u-flex u-cross-center u-gap-8"> <div class="u-flex u-cross-center u-gap-8">
<label class="u-small is-not-mobile" for="version">Version</label> <label class="u-small is-not-mobile" for="version">Version</label>
<div class="aw-select is-colored">
<select <Select
id="version"
on:change={selectVersion} on:change={selectVersion}
value={$page.params.version} value={$page.params.version}
> options={[
<option value="cloud">Cloud</option> { value: 'cloud', label: 'Cloud' },
{#each versions as version} ...versions.map((version) => ({
<option value={version}>{version}</option> value: version,
{/each} label: anyify(version)
</select> }))
<span class="icon-cheveron-down" aria-hidden="true" /> ]}
</div> />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,8 +1,10 @@
@use '../abstract' as *; @use '../abstract' as *;
.#{$p}-select { .#{$p}-select {
--p-select-min-width: #{pxToRem(130)};
all:unset; display:flex; align-items:center; position:relative; line-height:1; user-select:none; cursor:pointer; all:unset; display:flex; align-items:center; position:relative; line-height:1; user-select:none; cursor:pointer;
color:hsl(var(--aw-color-primary)); font-size:pxToRem(14); min-width: pxToRem(130); height: pxToRem(30); color:hsl(var(--aw-color-primary)); font-size:pxToRem(14); min-width: var(--p-select-min-width); height: pxToRem(30);
select, .physical-select { select, .physical-select {
all:unset; display:flex; align-items:center; all:unset; display:flex; align-items:center;
@@ -129,6 +131,9 @@
padding-block: pxToRem(4); padding-block: pxToRem(4);
box-sizing: border-box; box-sizing: border-box;
max-height: pxToRem(300) !important;
overflow-y: auto;
@mixin light-mode() { @mixin light-mode() {
--p-select-menu-bg-color: var(--aw-color-white); --p-select-menu-bg-color: var(--aw-color-white);
--p-select-menu-border-color: var(--aw-color-black) / 0.06; --p-select-menu-border-color: var(--aw-color-black) / 0.06;
@@ -155,6 +160,12 @@
&-label { &-label {
margin-block-end: pxToRem(4); margin-block-end: pxToRem(4);
padding-inline-start: pxToRem(4);
font-weight: 500;
}
.#{$p}-select-option {
padding-inline-start: pxToRem(16);
} }
} }
@@ -166,6 +177,7 @@
padding-block: pxToRem(4); padding-block: pxToRem(4);
padding-inline: pxToRem(4); padding-inline: pxToRem(4);
border-radius: pxToRem(6); border-radius: pxToRem(6);
scroll-margin-block: pxToRem(8);
cursor: pointer; cursor: pointer;