Merge branch 'main' into button-component

This commit is contained in:
Jesse Winton
2025-04-04 10:16:28 -04:00
8 changed files with 211 additions and 140 deletions

View File

@@ -27,7 +27,7 @@
"dependencies": {
"@number-flow/svelte": "^0.3.3",
"h3": "^1.14.0",
"melt": "^0.28.0",
"melt": "^0.28.2",
"posthog-js": "^1.210.2",
"sharp": "^0.33.5"
},

10
pnpm-lock.yaml generated
View File

@@ -15,8 +15,8 @@ importers:
specifier: ^1.14.0
version: 1.15.1
melt:
specifier: ^0.28.0
version: 0.28.0(@floating-ui/dom@1.6.13)(svelte@5.25.6)
specifier: ^0.28.2
version: 0.28.2(@floating-ui/dom@1.6.13)(svelte@5.25.6)
posthog-js:
specifier: ^1.210.2
version: 1.230.4
@@ -2726,8 +2726,8 @@ packages:
meilisearch@0.37.0:
resolution: {integrity: sha512-LdbK6JmRghCawrmWKJSEQF0OiE82md+YqJGE/U2JcCD8ROwlhTx0KM6NX4rQt0u0VpV0QZVG9umYiu3CSSIJAQ==}
melt@0.28.0:
resolution: {integrity: sha512-kiqaTgNB/IkADmUfJZKROqQ3z+isal8LjLhckQANqjfjggIosHM8M7RO3Og7IQ12zK06nLnwanL80SuTPhblrw==}
melt@0.28.2:
resolution: {integrity: sha512-55DGQ4B3bHKnDnK1ECJ46D+xythNKvuil60k0RXWJ3eEY5XsGjT6WZPVpRooYLfMlAki2J7JCWglCMYzvXwxVw==}
peerDependencies:
'@floating-ui/dom': ^1.6.0
svelte: ^5.0.0
@@ -6407,7 +6407,7 @@ snapshots:
transitivePeerDependencies:
- encoding
melt@0.28.0(@floating-ui/dom@1.6.13)(svelte@5.25.6):
melt@0.28.2(@floating-ui/dom@1.6.13)(svelte@5.25.6):
dependencies:
'@floating-ui/dom': 1.6.13
jest-axe: 9.0.0

View File

@@ -164,39 +164,39 @@
/* Font sizes */
--text-x-micro: 0.625rem;
--text-x-micro--line-height: 0.875rem;
--text-x-micro--tracking: var(--tracking-tighter);
--text-x-micro--letter-spacing: var(--tracking-tighter);
--text-micro: 0.75rem;
--text-micro--line-height: 1rem;
--text-micro--tracking: var(--tracking-tighter);
--text-micro--letter-spacing: var(--tracking-tighter);
--text-caption: 0.875rem;
--text-caption--line-height: 1.375rem;
--text-caption--tracking: var(--tracking-tight);
--text-caption--letter-spacing: var(--tracking-tight);
--text-sub-body: clamp(0.875rem, 2vw, 1rem);
--text-sub-body--line-height: 1.375rem;
--text-sub-body--tracking: var(--tracking-tight);
--text-sub-body--letter-spacing: var(--tracking-tight);
--text-body: clamp(1rem, 2.5vw, 1.125rem);
--text-body--line-height: clamp(1.375rem, 3vw, 1.625rem);
--text-body--tracking: var(--tracking-tight);
--text-body--letter-spacing: var(--tracking-tight);
--text-paragraph-md: 1rem;
--text-paragraph-md--line-height: 1.625rem;
--text-paragraph-md--tracking: var(--tracking-tight);
--text-paragraph-md--letter-spacing: var(--tracking-tight);
--text-paragraph-lg: 1.125rem;
--text-paragraph-lg--line-height: 1.75rem;
--text-paragraph-lg--tracking: var(--tracking-tight);
--text-paragraph-lg--letter-spacing: var(--tracking-tight);
--text-description: clamp(1.125rem, 3vw, 1.25rem);
--text-description--line-height: clamp(1.625rem, 3.5vw, 1.75rem);
--text-description--tracking: var(--tracking-tighter);
--text-description--letter-spacing: var(--tracking-tighter);
--text-label: 1.5rem;
--text-label--line-height: 1.75rem;
--text-title: clamp(2rem, 5vw, 2.5rem);
--text-title--line-height: clamp(2.125rem, 5.5vw, 2.75rem);
--text-title--tracking: var(--tracking-squeezed);
--text-title--letter-spacing: var(--tracking-squeezed);
--text-display: clamp(3rem, 7vw, 4rem);
--text-display--line-height: clamp(3.125rem, 7.5vw, 4.25rem);
--text-display--tracking: var(--tracking-compressed);
--text-display--letter-spacing: var(--tracking-compressed);
--text-headline: clamp(3.5rem, 8vw, 5.5rem);
--text-headline--line-height: clamp(3.5rem, 8.5vw, 5.75rem);
--text-headline--tracking: var(--tracking-compressed);
--text-headline--letter-spacing: var(--tracking-compressed);
/* Letter spacing */
--tracking-*: initial;
@@ -208,11 +208,14 @@
--tracking-loose: 0.08em;
}
@layer components {
.container {
@apply mx-auto box-content max-w-[75rem] px-5;
}
@utility container {
margin-inline: auto;
padding-inline: calc(var(--spacing) * 5);
box-sizing: box-content;
max-width: 75rem;
}
@layer components {
.mask {
mask-image: linear-gradient(
to var(--mask-direction, top),

View File

@@ -6,11 +6,11 @@
interface Props {
size?: 'default' | 'medium' | 'big';
gap?: number;
header: Snippet;
header?: Snippet;
children: Snippet;
}
let { size = 'default', gap = 32, header, children }: Props = $props();
const { size = 'default', gap = 32, header, children }: Props = $props();
let scroll = 0;
function calculateScrollAmount(prev = false) {
@@ -51,7 +51,9 @@
<div>
<div class="mt-2 flex flex-wrap items-center">
{@render header()}
{#if header}
{@render header()}
{/if}
<div class="nav ml-auto flex items-end gap-3">
<button
class="web-icon-button"

View File

@@ -1,121 +1,115 @@
<script lang="ts" module>
import type { Writable } from 'svelte/store';
import { writable, type Writable } from 'svelte/store';
import { Tabs } from 'melt/builders';
export type TabsItemProps = {
id: string;
title: string;
};
export type TabsContext = Writable<{
content: ReturnType<typeof createTabs>['elements']['content'];
triggers: Map<string, string>;
triggers: Array<TabsItemProps>;
tabs: Tabs<string>;
}>;
</script>
<script lang="ts">
import Select from '$lib/components/Select.svelte';
import { classNames } from '$lib/utils/classnames';
import { createTabs } from '@melt-ui/svelte';
import { setContext, type Snippet } from 'svelte';
import { writable } from 'svelte/store';
import { Select } from '$lib/components';
interface Props {
children: Snippet;
}
const { children }: Props = $props();
const {
elements: { root, list, content, trigger },
states: { value }
} = createTabs();
const tabs = new Tabs<string>({
value: ''
});
const ctx = setContext<TabsContext>(
'tabs',
writable({
content,
triggers: new Map()
triggers: [],
tabs
})
);
setContext('tabs-selection', value);
$effect(() => {
if ($ctx.triggers.length > 0 && !$ctx.tabs.value) {
$ctx.tabs.value = $ctx.triggers[0].id;
}
});
type TabsProps = {
children: Snippet;
};
const { children }: TabsProps = $props();
</script>
<div class="web-card is-normal mt-4" {...$root} use:root>
<div
class="dark:bg-greyscale-850/90 mt-4 mb-8 flex flex-col gap-1 rounded-2xl border border-black/8 bg-white/90 px-6 pt-4 pb-6 outline-0 dark:border-white/10"
>
<div
class="tabs flex items-center gap-4 overflow-scroll"
style="scrollbar-width: none; -ms-overflow-style: none;"
class="flex items-center gap-4 overflow-scroll [-ms-overflow-style:none] [scrollbard-width:none]"
>
<ul class="tabs-list hidden items-center gap-4 sm:flex" {...$list} use:list>
{#each Array.from($ctx.triggers.entries()).slice(0, 7) as [id, title]}
<li
class="tabs-item shrink-0 rounded-t-[0.625rem] text-center hover:bg-white/4"
class:text-[var(--color-primary)]={$value === id}
<div class="hidden items-center gap-4 sm:flex" {...tabs.triggerList}>
{#each $ctx.triggers.slice(0, 7) as { title, id }}
<button
class={classNames(
'shrink-0 rounded-t-[0.625rem] text-center hover:bg-white/4',
'relative cursor-pointer bg-clip-padding px-1 py-[0.625rem] font-light outline-none',
'after:relative after:top-1 after:bottom-0 after:block after:h-px after:transition-all',
{
'after:bg-[var(--color-primary)]': tabs.value === id
}
)}
{...tabs.getTrigger(id)}
>
<button
class={classNames(
'tabs-button relative cursor-pointer bg-clip-padding px-1 py-[0.625rem] font-light outline-none',
'after:relative after:top-1 after:bottom-0 after:block after:h-px after:transition-all',
{
'after:bg-[var(--color-primary)]': $value === id
}
)}
{...$trigger(id)}
use:trigger>{title}</button
>
</li>
{title}
</button>
{/each}
{#if Array.from($ctx.triggers.entries()).slice(7).length}
{@const entries = Array.from($ctx.triggers.entries())}
{@const desktopOptions = entries.slice(7)}
<li>
<Select
initialLabel="More"
options={desktopOptions.map(([value, label]) => {
return {
value,
label
};
})}
bind:value={$value}
/>
</li>
{#if $ctx.triggers.slice(7).length}
{@const desktopOptions = $ctx.triggers.slice(7)}
<Select
initialLabel="More"
options={desktopOptions.map(({ id, title }) => {
return {
value: id,
label: title
};
})}
bind:value={$ctx.tabs.value}
/>
{/if}
</ul>
<ul class="tabs-list flex items-center gap-4 sm:hidden" {...$list} use:list>
{#each Array.from($ctx.triggers.entries()).slice(0, 2) as [id, title]}
<li
class="tabs-item shrink-0 rounded-t-[0.625rem] text-center hover:bg-white/4"
class:text-[var(--color-primary)]={$value === id}
</div>
<div class="flex items-center gap-4 sm:hidden" {...tabs.triggerList}>
{#each $ctx.triggers.slice(0, 2) as { title, id }}
<button
class={classNames(
'shrink-0 rounded-t-[0.625rem] text-center hover:bg-white/4',
'relative cursor-pointer bg-clip-padding px-1 py-[0.625rem] font-light outline-none',
'after:relative after:top-1 after:bottom-0 after:block after:h-px after:transition-all',
{
'after:bg-[var(--color-primary)]': tabs.value === id
}
)}
{...tabs.getTrigger(id)}
>
<button
class={classNames(
'tabs-button relative cursor-pointer bg-clip-padding px-1 py-[0.625rem] font-light outline-none',
'after:relative after:top-1 after:bottom-0 after:block after:h-px after:transition-all',
{
'after:bg-[var(--color-primary)]': $value === id
}
)}
{...$trigger(id)}
use:trigger
>
{title}
</button>
</li>
{title}
</button>
{/each}
{#if Array.from($ctx.triggers.entries()).slice(2).length}
{@const entries = Array.from($ctx.triggers.entries())}
{@const desktopOptions = entries.slice(2)}
<li>
<Select
initialLabel="More"
options={desktopOptions.map(([value, label]) => {
return {
value,
label
};
})}
bind:value={$value}
/>
</li>
{#if $ctx.triggers.slice(2).length}
{@const desktopOptions = $ctx.triggers.slice(7)}
<Select
initialLabel="More"
options={desktopOptions.map(({ id, title }) => {
return {
value: id,
label: title
};
})}
bind:value={$ctx.tabs.value}
/>
{/if}
</ul>
</div>
</div>
{@render children?.()}
{@render children()}
</div>

View File

@@ -1,25 +1,19 @@
<script lang="ts">
import { getContext, type Snippet } from 'svelte';
import type { TabsContext } from './Tabs.svelte';
interface Props {
id: string;
title: string;
children: Snippet;
}
const { id, title, children }: Props = $props();
import { type TabsContext, type TabsItemProps } from './Tabs.svelte';
const ctx = getContext<TabsContext>('tabs');
const { content } = $ctx;
const { id, title, children }: TabsItemProps & { children: Snippet } = $props();
ctx.update((n) => {
n.triggers.set(id, title);
return n;
$effect(() => {
ctx.update((context) => {
context.triggers.push({ id, title });
return context;
});
});
</script>
<div class="web-u-sep-block-start pt-4" {...$content(id)} use:content>
<div class="border-smooth border-t pt-4" {...$ctx.tabs.getContent(id)}>
{@render children()}
</div>

View File

@@ -6,7 +6,7 @@ description: Learn how to manage team invites in Appwrite. Implement both client
Appwrite provides two approaches for adding members to teams: client-side email invites and server-side custom flows. Each approach serves different use cases and offers unique benefits.
# Invite client-side {% #client-side %}
# Invite client-side
Client-side email invites are perfect for implementing user-to-user invitations, allowing your users to invite others to join their teams, organizations, or shared resources. When creating a membership, Appwrite:
1. Creates a new user account if one doesn't exist for the email address
@@ -91,7 +91,7 @@ val response = teams.createMembership(
```
{% /multicode %}
## Accept invitations {% #accept-invitations %}
## Accept invitations
For client-side email invites, users must accept the invitation to join the team. The acceptance flow:
1. User receives an email with an invitation link containing a secret token
@@ -171,7 +171,7 @@ val response = teams.updateMembershipStatus(
```
{% /multicode %}
# Server-side custom flows {% #server-side %}
# Server-side custom flows
Server-side membership creation bypasses the email invitation process, allowing direct member addition. This approach:
1. Creates an active membership immediately
@@ -258,11 +258,11 @@ val response = teams.createMembership(
```
{% /multicode %}
# Manage memberships {% #manage-memberships %}
# Manage memberships
Once team memberships are created, you'll need to manage their lifecycle. This includes checking status, updating roles, and removing members when necessary.
## Check membership status {% #status %}
## Check membership status
Before performing actions on team memberships, you often need to verify a user's current status within a team. The process differs between client-side and server-side implementations.
@@ -497,7 +497,7 @@ teamsList.teams.forEach { team ->
```
{% /multicode %}
## Remove members {% #remove-members %}
## Remove members
Team owners can remove members or users can leave teams:
@@ -561,11 +561,11 @@ teams.deleteMembership(
```
{% /multicode %}
# Manage team permissions {% #permissions %}
# Manage team permissions
Teams in Appwrite use a role-based access control (RBAC) system. Each team member can be assigned one or more roles that define their permissions within the team.
## Update roles {% #update-roles %}
## Update roles
You can assign roles when creating a membership or update them later. Note that only team members with the owner role can update other members' roles:
@@ -637,7 +637,7 @@ teams.updateMembership(
```
{% /multicode %}
## Check role access {% #check-role-access %}
## Check role access
You can verify if a user has specific roles:

View File

@@ -135,6 +135,11 @@ query {
}
}
```
```http
GET /v1/databases/<DATABASE_ID>/collections/<COLLECTION_ID>/documents?queries[]=%7B%22method%22%3A%22equal%22%2C%22attribute%22%3A%22title%22%2C%22values%22%3A%5B%22Avatar%22%2C%22Lord%20of%20the%20Rings%22%5D%7D&queries[]=%7B%22method%22%3A%22greaterThan%22%2C%22attribute%22%3A%22year%22%2C%22values%22%3A%5B1999%5D%7D HTTP/1.1
Content-Type: application/json
X-Appwrite-Project: <PROJECT_ID>
```
{% /multicode %}
# Query operators {% #query-operators %}
@@ -165,6 +170,9 @@ Query::select(["name", "title"])
```swift
Query.select(["name", "title"])
```
```http
{"method":"select","values":["name","title"]}
```
{% /multicode %}
## Comparison operators {% #comparison %}
@@ -195,6 +203,9 @@ Query::equal("title", ["Iron Man"])
```swift
Query.equal("title", value: ["Iron Man"])
```
```http
{"method":"equal","attribute":"title","values":["Iron Man"]}
```
{% /multicode %}
### Not equal {% #not-equal %}
@@ -223,6 +234,9 @@ Query::notEqual("title", ["Iron Man"])
```swift
Query.notEqual("title", value: ["Iron Man"])
```
```http
{"method":"notEqual","attribute":"title","values":["Iron Man"]}
```
{% /multicode %}
### Less than {% #less-than %}
@@ -251,6 +265,9 @@ Query::lessThan("score", 10)
```swift
Query.lessThan("score", value: 10)
```
```http
{"method":"lessThan","attribute":"score","values":[10]}
```
{% /multicode %}
### Less than or equal {% #less-than-equal %}
@@ -279,6 +296,9 @@ Query::lessThanEqual("score", 10)
```swift
Query.lessThanEqual("score", value: 10)
```
```http
{"method":"lessThanEqual","attribute":"score","values":[10]}
```
{% /multicode %}
### Greater than {% #greater-than %}
@@ -307,6 +327,9 @@ Query::greaterThan("score", 10)
```swift
Query.greaterThan("score", value: 10)
```
```http
{"method":"greaterThan","attribute":"score","values":[10]}
```
{% /multicode %}
### Greater than or equal {% #greater-than-equal %}
@@ -335,6 +358,9 @@ Query::greaterThanEqual("score", 10)
```swift
Query.greaterThanEqual("score", value: 10)
```
```http
{"method":"greaterThanEqual","attribute":"score","values":[10]}
```
{% /multicode %}
### Between {% #between %}
@@ -363,6 +389,9 @@ Query::between("price", 5, 10)
```swift
Query.between("price", start: 5, end: 10)
```
```http
{"method":"between","attribute":"price","values":[5,10]}
```
{% /multicode %}
## Null checks {% #null-checks %}
@@ -393,6 +422,9 @@ Query::isNull("name")
```swift
Query.isNull("name")
```
```http
{"method":"isNull","attribute":"name"}
```
{% /multicode %}
### Is not null {% #is-not-null %}
@@ -421,6 +453,9 @@ Query::isNotNull("name")
```swift
Query.isNotNull("name")
```
```http
{"method":"isNotNull","attribute":"name"}
```
{% /multicode %}
## String operations {% #string-operations %}
@@ -451,6 +486,9 @@ Query::startsWith("name", "Once upon a time")
```swift
Query.startsWith("name", value: "Once upon a time")
```
```http
{"method":"startsWith","attribute":"name","values":["Once upon a time"]}
```
{% /multicode %}
### Ends with {% #ends-with %}
@@ -479,6 +517,9 @@ Query::endsWith("name", "happily ever after.")
```swift
Query.endsWith("name", value: "happily ever after.")
```
```http
{"method":"endsWith","attribute":"name","values":["happily ever after."]}
```
{% /multicode %}
### Contains {% #contains %}
@@ -535,6 +576,13 @@ Query.contains("ingredients", value: ['apple', 'banana'])
// For strings
Query.contains("name", value: "Tom")
```
```http
# For arrays
{"method":"contains","attribute":"ingredients","values":["apple","banana"]}
# For strings
{"method":"contains","attribute":"name","values":["Tom"]}
```
{% /multicode %}
### Search {% #search %}
@@ -563,6 +611,9 @@ Query::search("text", "key words")
```swift
Query.search("text", value: "key words")
```
```http
{"method":"search","attribute":"text","values":["key words"]}
```
{% /multicode %}
## Logical operators {% #logical-operators %}
@@ -614,6 +665,9 @@ Query.and([
Query.greaterThan("size", value: 5)
])
```
```http
{"method":"and","values":[{"method":"lessThan","attribute":"size","values":[10]},{"method":"greaterThan","attribute":"size","values":[5]}]}
```
{% /multicode %}
### OR {% #or %}
@@ -663,6 +717,9 @@ Query.or([
Query.greaterThan("size", value: 10)
])
```
```http
{"method":"or","values":[{"method":"lessThan","attribute":"size","values":[5]},{"method":"greaterThan","attribute":"size","values":[10]}]}
```
{% /multicode %}
## Ordering {% #ordering %}
@@ -693,6 +750,9 @@ Query::orderDesc("attribute")
```swift
Query.orderDesc("attribute")
```
```http
{"method":"orderDesc","attribute":"attribute"}
```
{% /multicode %}
### Order ascending {% #order-asc %}
@@ -721,6 +781,9 @@ Query::orderAsc("attribute")
```swift
Query.orderAsc("attribute")
```
```http
{"method":"orderAsc","attribute":"attribute"}
```
{% /multicode %}
## Pagination {% #pagination %}
@@ -751,6 +814,9 @@ Query::limit(25)
```swift
Query.limit(25)
```
```http
{"method":"limit","values":[25]}
```
{% /multicode %}
### Offset {% #offset %}
@@ -779,6 +845,9 @@ Query::offset(0)
```swift
Query.offset(0)
```
```http
{"method":"offset","values":[0]}
```
{% /multicode %}
### Cursor after {% #cursor-after %}
@@ -807,6 +876,9 @@ Query::cursorAfter("62a7...f620")
```swift
Query.cursorAfter("62a7...f620")
```
```http
{"method":"cursorAfter","values":["62a7...f620"]}
```
{% /multicode %}
### Cursor before {% #cursor-before %}
@@ -835,6 +907,9 @@ Query::cursorBefore("62a7...a600")
```swift
Query.cursorBefore("62a7...a600")
```
```http
{"method":"cursorBefore","values":["62a7...a600"]}
```
{% /multicode %}
# Complex queries {% #complex-queries %}
@@ -896,6 +971,9 @@ results = databases.list_documents(
]
)
```
```http
{"method":"or","values":[{"method":"and","values":[{"method":"equal","attribute":"category","values":["books"]},{"method":"lessThan","attribute":"price","values":[20]}]},{"method":"and","values":[{"method":"equal","attribute":"category","values":["magazines"]},{"method":"lessThan","attribute":"price","values":[10]}]}]}
```
{% /multicode %}
This example demonstrates how to combine `OR` and `AND` operations. The query uses `Query.or()` to match either condition: books under $20 OR magazines under $10.