@ripple-ui/svelte 0.8.2 → 0.9.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/discover/CreatorAvatar.svelte +37 -0
- package/dist/discover/CreatorAvatar.svelte.d.ts +10 -0
- package/dist/discover/CreatorLine.svelte +18 -0
- package/dist/discover/CreatorLine.svelte.d.ts +8 -0
- package/dist/discover/DiscoverChips.svelte +15 -2
- package/dist/discover/DiscoverFilters.svelte +56 -41
- package/dist/discover/DiscoverHeader.svelte +33 -10
- package/dist/discover/DiscoverHeader.svelte.d.ts +3 -0
- package/dist/discover/DiscoverRow.svelte +36 -10
- package/dist/discover/DiscoverRow.svelte.d.ts +7 -0
- package/dist/discover/DiscoverSection.svelte +18 -10
- package/dist/discover/DiscoverSkeleton.svelte +5 -3
- package/dist/discover/DiscoverTile.svelte +52 -17
- package/dist/discover/DiscoverTile.svelte.d.ts +7 -0
- package/dist/discover/ItemActions.svelte +46 -17
- package/dist/discover/ItemActions.svelte.d.ts +6 -0
- package/dist/discover/ItemDetail.svelte +280 -0
- package/dist/discover/ItemDetail.svelte.d.ts +32 -0
- package/dist/discover/ItemMenu.svelte +61 -0
- package/dist/discover/ItemMenu.svelte.d.ts +13 -0
- package/dist/discover/LiveFrame.svelte +162 -0
- package/dist/discover/LiveFrame.svelte.d.ts +21 -0
- package/dist/discover/WhatsAppQrCard.svelte +302 -0
- package/dist/discover/WhatsAppQrCard.svelte.d.ts +18 -0
- package/dist/discover/frame.d.ts +7 -0
- package/dist/discover/frame.js +27 -0
- package/dist/discover/href.d.ts +7 -1
- package/dist/discover/href.js +16 -6
- package/dist/discover/index.d.ts +7 -2
- package/dist/discover/index.js +15 -7
- package/dist/discover/item.d.ts +14 -0
- package/dist/discover/item.js +41 -2
- package/dist/discover/layout.d.ts +1 -1
- package/dist/discover/layout.js +1 -1
- package/dist/discover/sections.d.ts +39 -0
- package/dist/discover/sections.js +84 -0
- package/dist/discover/types.d.ts +17 -0
- package/dist/discover/types.js +5 -2
- package/dist/discover/whatsapp-card-export.d.ts +9 -0
- package/dist/discover/whatsapp-card-export.js +72 -0
- package/dist/discover/whatsapp-card.d.ts +106 -0
- package/dist/discover/whatsapp-card.js +226 -0
- package/dist/discover/whatsapp-qr.d.ts +7 -0
- package/dist/discover/whatsapp-qr.js +20 -0
- package/dist/figures/bike-gears/figure.js +2 -6
- package/dist/figures/fission-reactor/figure.js +2 -6
- package/dist/figures/heart/figure.js +2 -6
- package/dist/figures/ion-thruster/figure.js +2 -6
- package/dist/figures/particle-collider/figure.js +2 -6
- package/dist/figures/rocket-engine/figure.js +2 -6
- package/dist/figures/style.d.ts +2 -0
- package/dist/figures/style.js +18 -0
- package/dist/figures/tokamak/figure.js +2 -6
- package/dist/manifest/entries/chart.js +1 -1
- package/dist/manifest/entries/markdown.js +2 -2
- package/dist/manifest.json +5 -5
- package/dist/styles.css +11 -10
- package/dist/utils/markdown-ext.d.ts +6 -0
- package/dist/utils/markdown-ext.js +68 -0
- package/dist/utils/mermaid.d.ts +94 -0
- package/dist/utils/mermaid.js +144 -0
- package/dist/utils/sanitize-html.d.ts +6 -0
- package/dist/utils/sanitize-html.js +15 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/widgets/data/Chart.svelte +34 -6
- package/dist/widgets/data/chart-layout.d.ts +4 -4
- package/dist/widgets/data/chart-layout.js +9 -8
- package/dist/widgets/display/Markdown.svelte +147 -3
- package/dist/widgets/display/Markdown.svelte.d.ts +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!-- discover/CreatorAvatar.svelte — a creator's round avatar for the Discover
|
|
2
|
+
cards and the detail view: ripple's Avatar with the picture (only an
|
|
3
|
+
http(s) avatarUrl, through safeUrl) and the name's initials as the
|
|
4
|
+
fallback, which is what the server markup and a broken picture show.
|
|
5
|
+
Decorative: the name always sits beside it in text. Internal to the card
|
|
6
|
+
set (not exported). -->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import { safeUrl } from '@ripple-ui/core';
|
|
9
|
+
import * as Avatar from '../components/ui/avatar/index.js';
|
|
10
|
+
import { cn } from '../utils.js';
|
|
11
|
+
import { creatorInitials, httpUrl } from './item.js';
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
name,
|
|
15
|
+
avatarUrl,
|
|
16
|
+
size = 'sm',
|
|
17
|
+
class: className,
|
|
18
|
+
}: {
|
|
19
|
+
name: string;
|
|
20
|
+
avatarUrl?: string | null;
|
|
21
|
+
/** sm: 20px on cards; lg: 32px on the detail view. */
|
|
22
|
+
size?: 'sm' | 'lg';
|
|
23
|
+
class?: string;
|
|
24
|
+
} = $props();
|
|
25
|
+
|
|
26
|
+
const src = $derived(httpUrl(avatarUrl));
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<Avatar.Root
|
|
30
|
+
size={size === 'lg' ? 'default' : 'sm'}
|
|
31
|
+
class={cn(size === 'sm' ? 'size-5 data-[size=sm]:size-5' : 'size-8', className)}
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
data-testid="discover-creator-avatar"
|
|
34
|
+
>
|
|
35
|
+
{#if src}<Avatar.Image src={safeUrl(src, { kind: 'resource' })} alt="" />{/if}
|
|
36
|
+
<Avatar.Fallback class={cn('bg-secondary font-semibold text-secondary-foreground', size === 'sm' ? 'text-[9px]' : 'text-xs')}>{creatorInitials(name)}</Avatar.Fallback>
|
|
37
|
+
</Avatar.Root>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
name: string;
|
|
3
|
+
avatarUrl?: string | null;
|
|
4
|
+
/** sm: 20px on cards; lg: 32px on the detail view. */
|
|
5
|
+
size?: 'sm' | 'lg';
|
|
6
|
+
class?: string;
|
|
7
|
+
};
|
|
8
|
+
declare const CreatorAvatar: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type CreatorAvatar = ReturnType<typeof CreatorAvatar>;
|
|
10
|
+
export default CreatorAvatar;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!-- discover/CreatorLine.svelte — "by <name>" with the creator's small avatar,
|
|
2
|
+
under a card's title (tile and row). Draws nothing without a creator name,
|
|
3
|
+
so a listing whose maker did not opt in leaves no gap or placeholder.
|
|
4
|
+
Internal to the card set (not exported). -->
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import CreatorAvatar from './CreatorAvatar.svelte';
|
|
7
|
+
import type { DiscoverItem } from './types.js';
|
|
8
|
+
|
|
9
|
+
let { creator, class: className }: { creator: DiscoverItem['creator']; class?: string } = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
{#if creator?.name}
|
|
13
|
+
<!-- Not cn(): tailwind-merge reads text-footnote and text-muted-foreground as one group. -->
|
|
14
|
+
<p class={`m-0 flex min-w-0 items-center gap-1.5 text-footnote text-muted-foreground ${className ?? ''}`} data-testid="discover-creator">
|
|
15
|
+
<CreatorAvatar name={creator.name} avatarUrl={creator.avatarUrl} />
|
|
16
|
+
<span class="truncate">by {creator.name}</span>
|
|
17
|
+
</p>
|
|
18
|
+
{/if}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DiscoverItem } from './types.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
creator: DiscoverItem['creator'];
|
|
4
|
+
class?: string;
|
|
5
|
+
};
|
|
6
|
+
declare const CreatorLine: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type CreatorLine = ReturnType<typeof CreatorLine>;
|
|
8
|
+
export default CreatorLine;
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
a server-rendered page; to make it toggle off, point the selected chip's href
|
|
5
5
|
at the address without that filter. A chip without `href` is a button
|
|
6
6
|
(aria-pressed) that calls `onchange` with its value; the consumer decides
|
|
7
|
-
what a second press means.
|
|
7
|
+
what a second press means.
|
|
8
|
+
|
|
9
|
+
The chips sit in ONE row that scrolls sideways (snap, no visible bar) with
|
|
10
|
+
a fade on the trailing edge; the end padding keeps the last chip clear of
|
|
11
|
+
the fade once scrolled to. Every chip is a real link or button, so Tab
|
|
12
|
+
reaches each one and the browser scrolls it into view. -->
|
|
8
13
|
<script lang="ts">
|
|
9
14
|
import { safeUrl } from '@ripple-ui/core';
|
|
10
15
|
import { Button } from '../components/ui/button/index.js';
|
|
@@ -26,7 +31,15 @@
|
|
|
26
31
|
} = $props();
|
|
27
32
|
</script>
|
|
28
33
|
|
|
29
|
-
<div
|
|
34
|
+
<div
|
|
35
|
+
class={cn(
|
|
36
|
+
'-m-1 flex snap-x scroll-px-1 flex-nowrap gap-2 overflow-x-auto p-1 pr-8 [mask-image:linear-gradient(to_right,black_calc(100%-2rem),transparent)] [scrollbar-width:none] *:shrink-0 *:snap-start [&::-webkit-scrollbar]:hidden',
|
|
37
|
+
className,
|
|
38
|
+
)}
|
|
39
|
+
role="group"
|
|
40
|
+
aria-label={label}
|
|
41
|
+
data-testid="discover-chips"
|
|
42
|
+
>
|
|
30
43
|
{#each options as o (o.value)}
|
|
31
44
|
{@const on = value === o.value}
|
|
32
45
|
{#if o.href}
|
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
icons up by name in the whole Lucide barrel (~800 KB a consumer cannot tree
|
|
9
9
|
shake); the view icons here are imported one by one. The view toggle shows
|
|
10
10
|
only with `onview` or `viewHref`. The bar paints bg-background; the app adds
|
|
11
|
-
its glass class through `class`.
|
|
11
|
+
its glass class through `class`.
|
|
12
|
+
|
|
13
|
+
The kind tabs scroll sideways in their own track (snap, trailing fade, no
|
|
14
|
+
visible bar); the selected segment paints its own surface, so any widths
|
|
15
|
+
line up and the server markup is final. The view toggle never shrinks and
|
|
16
|
+
goes icon-only (labels kept for screen readers) when the bar is under
|
|
17
|
+
36rem, so it stays on screen on a phone. -->
|
|
12
18
|
<script lang="ts">
|
|
13
19
|
import { safeUrl } from '@ripple-ui/core';
|
|
14
20
|
import type { Component } from 'svelte';
|
|
@@ -48,59 +54,68 @@
|
|
|
48
54
|
DISCOVER_VIEWS.map((v) => ({ value: v, label: VIEW_META[v].label, href: viewHref?.(v), Icon: VIEW_META[v].Icon })),
|
|
49
55
|
);
|
|
50
56
|
|
|
57
|
+
/** On mount, slide the kind track so a selected tab past its edge is in view
|
|
58
|
+
* (a link-mode page opened at ?kind=music). Horizontal only: the page never jumps. */
|
|
59
|
+
function revealSelected(track: HTMLElement) {
|
|
60
|
+
const on = track.querySelector<HTMLElement>('[aria-current="true"], [aria-checked="true"]');
|
|
61
|
+
if (!on) return;
|
|
62
|
+
const end = on.offsetLeft + on.offsetWidth;
|
|
63
|
+
if (end > track.scrollLeft + track.clientWidth - 24) track.scrollLeft = end - track.clientWidth + 24;
|
|
64
|
+
}
|
|
65
|
+
|
|
51
66
|
function pickView(v: string) {
|
|
52
67
|
const next = DISCOVER_VIEWS.find((x) => x === v);
|
|
53
68
|
if (next) onview?.(next);
|
|
54
69
|
}
|
|
55
70
|
</script>
|
|
56
71
|
|
|
57
|
-
<!-- Segmented's size="sm" track
|
|
58
|
-
segments are radios (Segmented's own semantics); without it they are links.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
<!-- Segmented's size="sm" track and segment classes. With `onpick` the
|
|
73
|
+
segments are radios (Segmented's own semantics); without it they are links.
|
|
74
|
+
The selected segment paints its own surface (no sliding thumb), so the
|
|
75
|
+
server markup is already right and segments of any width line up. -->
|
|
76
|
+
{#snippet pills(options: readonly (DiscoverOption & { Icon?: Component })[], selected: string, onpick?: (value: string) => void, iconOnly = false)}
|
|
77
|
+
<div
|
|
78
|
+
role={onpick ? 'radiogroup' : undefined}
|
|
79
|
+
class="inline-flex h-7 w-max items-stretch rounded-full bg-ripple-border/60 p-0.5 text-[12px] select-none"
|
|
80
|
+
>
|
|
81
|
+
{#each options as o (o.value)}
|
|
82
|
+
{@const on = o.value === selected}
|
|
83
|
+
{@const segment = cn(
|
|
84
|
+
'inline-flex shrink-0 snap-start items-center justify-center gap-1.5 rounded-full px-2.5 font-medium whitespace-nowrap transition-colors duration-150 ease-ripple-out focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none motion-reduce:transition-none',
|
|
85
|
+
on ? 'bg-ripple-surface text-ripple-surface-foreground ring-1 ring-ripple-border' : 'text-ripple-muted-foreground hover:text-ripple-surface-foreground',
|
|
86
|
+
iconOnly && o.Icon && 'px-2 @xl/discover-filters:px-2.5',
|
|
87
|
+
)}
|
|
88
|
+
{#snippet body()}
|
|
89
|
+
{#if o.Icon}<o.Icon size={14} aria-hidden="true" />{/if}
|
|
90
|
+
<span class={cn(iconOnly && o.Icon && 'sr-only @xl/discover-filters:not-sr-only')}>{o.label}</span>
|
|
91
|
+
{/snippet}
|
|
92
|
+
{#if onpick}
|
|
93
|
+
<button type="button" role="radio" aria-checked={on} class={segment} title={iconOnly ? o.label : undefined} onclick={() => onpick(o.value)}>
|
|
94
|
+
{@render body()}
|
|
95
|
+
</button>
|
|
96
|
+
{:else}
|
|
97
|
+
<a href={safeUrl(o.href)} aria-current={on ? 'true' : undefined} class={segment} title={iconOnly ? o.label : undefined}>
|
|
98
|
+
{@render body()}
|
|
99
|
+
</a>
|
|
73
100
|
{/if}
|
|
74
|
-
|
|
75
|
-
{@const on = o.value === selected}
|
|
76
|
-
{@const segment = cn(
|
|
77
|
-
'relative z-10 inline-flex items-center justify-center gap-1.5 rounded-full px-2.5 font-medium whitespace-nowrap transition-colors duration-150 ease-ripple-out',
|
|
78
|
-
on ? 'text-ripple-surface-foreground' : 'text-ripple-muted-foreground hover:text-ripple-surface-foreground',
|
|
79
|
-
)}
|
|
80
|
-
{#if onpick}
|
|
81
|
-
<button type="button" role="radio" aria-checked={on} class={segment} onclick={() => onpick(o.value)}>
|
|
82
|
-
{#if o.Icon}<o.Icon size={14} />{/if}
|
|
83
|
-
{o.label}
|
|
84
|
-
</button>
|
|
85
|
-
{:else}
|
|
86
|
-
<a href={safeUrl(o.href)} aria-current={on ? 'true' : undefined} class={segment}>
|
|
87
|
-
{#if o.Icon}<o.Icon size={14} />{/if}
|
|
88
|
-
{o.label}
|
|
89
|
-
</a>
|
|
90
|
-
{/if}
|
|
91
|
-
{/each}
|
|
92
|
-
</div>
|
|
101
|
+
{/each}
|
|
93
102
|
</div>
|
|
94
103
|
{/snippet}
|
|
95
104
|
|
|
96
|
-
<div class={cn('sticky top-0 z-10 mt-6 border-b border-border bg-background', className)}>
|
|
97
|
-
<div class="mx-auto flex w-full max-w-[1280px] items-center gap-
|
|
98
|
-
<div
|
|
105
|
+
<div class={cn('@container/discover-filters sticky top-0 z-10 mt-6 border-b border-border bg-background', className)} data-testid="discover-filters">
|
|
106
|
+
<div class="mx-auto flex w-full max-w-[1280px] items-center gap-2 px-6 py-2.5 @xl/discover-filters:gap-3">
|
|
107
|
+
<div
|
|
108
|
+
class="relative -my-1 min-w-0 flex-1 snap-x scroll-px-1 overflow-x-auto py-1 pr-6 [mask-image:linear-gradient(to_right,black_calc(100%-1.5rem),transparent)] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
109
|
+
role="group"
|
|
110
|
+
aria-label="Filter by type"
|
|
111
|
+
data-testid="discover-kinds"
|
|
112
|
+
use:revealSelected
|
|
113
|
+
>
|
|
99
114
|
{@render pills(kinds, kind, kindLinks ? undefined : (v) => onkind?.(v))}
|
|
100
115
|
</div>
|
|
101
116
|
{#if viewHref || onview}
|
|
102
|
-
<div class="
|
|
103
|
-
{@render pills(viewOptions, view, viewHref ? undefined : pickView)}
|
|
117
|
+
<div class="shrink-0" role="group" aria-label="View">
|
|
118
|
+
{@render pills(viewOptions, view, viewHref ? undefined : pickView, true)}
|
|
104
119
|
</div>
|
|
105
120
|
{/if}
|
|
106
121
|
</div>
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
<!-- discover/DiscoverHeader.svelte — the top of the Discover page: the pitch
|
|
2
2
|
(a title whose `accent` half is painted in the primary colour, then a lead
|
|
3
3
|
paragraph), the search and audience chips as children, and an optional
|
|
4
|
-
`aside` beside it on wide
|
|
5
|
-
under an app page header; a page without one passes as="h1".
|
|
4
|
+
`aside` beside it on wide containers (the app's QR card). The heading is
|
|
5
|
+
an h2 under an app page header; a page without one passes as="h1".
|
|
6
|
+
|
|
7
|
+
It sizes off its own box (container `discover-header`), not the viewport,
|
|
8
|
+
because the app shell and the public page give it different widths. Narrow
|
|
9
|
+
(under 36rem): a title-2 heading, the lead hidden (it stays in the markup;
|
|
10
|
+
`narrowLead="full"` keeps it, for a landing hero whose lead is the pitch),
|
|
11
|
+
tighter gaps, and the aside stacked under the pitch; the QR card collapses
|
|
12
|
+
itself to a teaser row there. From 56rem the aside sits beside the pitch.
|
|
13
|
+
`class` lands on the inner grid (so `lg:items-start` and the like still
|
|
14
|
+
work); the container is a plain full-width wrapper around it. -->
|
|
6
15
|
<script lang="ts">
|
|
7
16
|
import type { Snippet } from 'svelte';
|
|
8
17
|
import { cn } from '../utils.js';
|
|
@@ -12,6 +21,7 @@
|
|
|
12
21
|
accent,
|
|
13
22
|
lead,
|
|
14
23
|
as = 'h2',
|
|
24
|
+
narrowLead = 'hide',
|
|
15
25
|
class: className,
|
|
16
26
|
children,
|
|
17
27
|
aside,
|
|
@@ -19,8 +29,11 @@
|
|
|
19
29
|
title: string;
|
|
20
30
|
/** Drawn after the title in the primary colour. */
|
|
21
31
|
accent?: string;
|
|
32
|
+
/** Shown on containers 36rem and wider; below that, see `narrowLead`. */
|
|
22
33
|
lead?: string;
|
|
23
34
|
as?: 'h1' | 'h2';
|
|
35
|
+
/** The lead under 36rem: 'hide' (default; Discover's search and chips come first) or 'full'. */
|
|
36
|
+
narrowLead?: 'hide' | 'full';
|
|
24
37
|
class?: string;
|
|
25
38
|
/** Search, chips: stacked under the lead. */
|
|
26
39
|
children?: Snippet;
|
|
@@ -28,13 +41,23 @@
|
|
|
28
41
|
} = $props();
|
|
29
42
|
</script>
|
|
30
43
|
|
|
31
|
-
<div class=
|
|
32
|
-
<div
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
<div class="@container/discover-header w-full" data-testid="discover-header">
|
|
45
|
+
<div
|
|
46
|
+
class={cn(
|
|
47
|
+
'mx-auto grid w-full max-w-[1280px] items-center gap-4 px-6 pt-2 @xl/discover-header:gap-9 @xl/discover-header:pt-4 @4xl/discover-header:grid-cols-[1.1fr_0.9fr]',
|
|
48
|
+
className,
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
<div class="flex min-w-0 flex-col gap-3 @xl/discover-header:gap-4">
|
|
52
|
+
<svelte:element this={as} class="text-title-2 font-semibold tracking-tight text-balance text-foreground @xl/discover-header:text-title-1">
|
|
53
|
+
{title}{#if accent}{' '}<span class="text-primary">{accent}</span>{/if}
|
|
54
|
+
</svelte:element>
|
|
55
|
+
{#if lead}
|
|
56
|
+
<!-- Not cn(): tailwind-merge reads text-body and text-muted-foreground as one group. -->
|
|
57
|
+
<p class={`max-w-prose text-body text-muted-foreground${narrowLead === 'hide' ? ' hidden @xl/discover-header:block' : ''}`}>{lead}</p>
|
|
58
|
+
{/if}
|
|
59
|
+
{@render children?.()}
|
|
60
|
+
</div>
|
|
61
|
+
{@render aside?.()}
|
|
38
62
|
</div>
|
|
39
|
-
{@render aside?.()}
|
|
40
63
|
</div>
|
|
@@ -3,8 +3,11 @@ type $$ComponentProps = {
|
|
|
3
3
|
title: string;
|
|
4
4
|
/** Drawn after the title in the primary colour. */
|
|
5
5
|
accent?: string;
|
|
6
|
+
/** Shown on containers 36rem and wider; below that, see `narrowLead`. */
|
|
6
7
|
lead?: string;
|
|
7
8
|
as?: 'h1' | 'h2';
|
|
9
|
+
/** The lead under 36rem: 'hide' (default; Discover's search and chips come first) or 'full'. */
|
|
10
|
+
narrowLead?: 'hide' | 'full';
|
|
8
11
|
class?: string;
|
|
9
12
|
/** Search, chips: stacked under the lead. */
|
|
10
13
|
children?: Snippet;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
<!-- discover/DiscoverRow.svelte — one Discover item in a list: a small copy of
|
|
2
|
-
the same picture, title
|
|
2
|
+
the same picture, title, one line and the creator line (only with a
|
|
3
|
+
creator), the usage count and published date, then the actions.
|
|
3
4
|
Built for scanning many items at once. With `href`, the art and the title are
|
|
4
5
|
links; the art link is decorative. `target` and `rel` pass through to
|
|
5
|
-
both, for consumers whose item pages are off-site
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
both, for consumers whose item pages are off-site; with `onopen` too, a
|
|
7
|
+
plain click opens the item in place. The meta column (usage, left out at
|
|
8
|
+
zero, over the published date) stays, empty if need be, so rows align. Renders fully on the server. Type scale as in the
|
|
9
|
+
app's own list rows: text-body title, text-footnote line, text-caption-1
|
|
10
|
+
usage. -->
|
|
8
11
|
<script lang="ts">
|
|
9
12
|
import { safeUrl } from '@ripple-ui/core';
|
|
10
13
|
import GitFork from '@lucide/svelte/icons/git-fork';
|
|
@@ -13,7 +16,9 @@
|
|
|
13
16
|
import ItemActions from './ItemActions.svelte';
|
|
14
17
|
import ItemArt from './ItemArt.svelte';
|
|
15
18
|
import { initialFor, tintFor } from './art.js';
|
|
16
|
-
import {
|
|
19
|
+
import { isPlainClick } from './href.js';
|
|
20
|
+
import { publishedDate, showUsage, usageLabel } from './item.js';
|
|
21
|
+
import CreatorLine from './CreatorLine.svelte';
|
|
17
22
|
import type { DiscoverItem } from './types.js';
|
|
18
23
|
|
|
19
24
|
let {
|
|
@@ -21,9 +26,11 @@
|
|
|
21
26
|
href,
|
|
22
27
|
target,
|
|
23
28
|
rel,
|
|
29
|
+
remixHref,
|
|
24
30
|
onremix,
|
|
25
31
|
onreport,
|
|
26
32
|
onopen,
|
|
33
|
+
oncopylink,
|
|
27
34
|
busy = false,
|
|
28
35
|
class: className,
|
|
29
36
|
}: {
|
|
@@ -33,12 +40,27 @@
|
|
|
33
40
|
/** Passed to both links. Off-site consumers pass target="_blank" rel="noopener". */
|
|
34
41
|
target?: string;
|
|
35
42
|
rel?: string;
|
|
43
|
+
/** Claim / Use this recipe and Remix as links (the website, to the app). Wins over `onremix`. */
|
|
44
|
+
remixHref?: string;
|
|
36
45
|
onremix?: (item: DiscoverItem) => void;
|
|
37
46
|
onreport?: (item: DiscoverItem) => void;
|
|
47
|
+
/** Opens the item in place. With `href`, a plain click on the art or title calls
|
|
48
|
+
* it instead of following the link; modified clicks and JS-off still navigate.
|
|
49
|
+
* For studio items it also drives the Play / View button. */
|
|
38
50
|
onopen?: (item: DiscoverItem) => void;
|
|
51
|
+
/** Copy link in the ⋯ menu, shown with `href`. Gets the item and that href. */
|
|
52
|
+
oncopylink?: (item: DiscoverItem, href: string) => void;
|
|
39
53
|
busy?: boolean;
|
|
40
54
|
class?: string;
|
|
41
55
|
} = $props();
|
|
56
|
+
|
|
57
|
+
const published = $derived(publishedDate(item));
|
|
58
|
+
|
|
59
|
+
function openInPlace(e: MouseEvent) {
|
|
60
|
+
if (!onopen || !isPlainClick(e)) return;
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
onopen(item);
|
|
63
|
+
}
|
|
42
64
|
</script>
|
|
43
65
|
|
|
44
66
|
<div class={cn('flex items-center gap-4 px-3 py-2.5', className)} role="listitem" data-testid={`discover-item-${item.id}`}>
|
|
@@ -47,7 +69,7 @@
|
|
|
47
69
|
<ItemArt imageUrl={item.imageUrl} title={item.title} mediaKind={item.mediaKind} tint={tintFor(item.id)} initial={initialFor(item.title)} />
|
|
48
70
|
{/snippet}
|
|
49
71
|
{#if href}
|
|
50
|
-
<a href={safeUrl(href)} {target} {rel} tabindex="-1" class="block">{@render art()}</a>
|
|
72
|
+
<a href={safeUrl(href)} {target} {rel} onclick={openInPlace} tabindex="-1" class="block">{@render art()}</a>
|
|
51
73
|
{:else}
|
|
52
74
|
{@render art()}
|
|
53
75
|
{/if}
|
|
@@ -55,16 +77,20 @@
|
|
|
55
77
|
<div class="flex min-w-0 flex-1 flex-col">
|
|
56
78
|
<span class="flex min-w-0 items-center gap-2">
|
|
57
79
|
{#if href}
|
|
58
|
-
<a href={safeUrl(href)} {target} {rel} class="truncate text-body font-semibold text-foreground hover:underline">{item.title}</a>
|
|
80
|
+
<a href={safeUrl(href)} {target} {rel} onclick={openInPlace} class="truncate text-body font-semibold text-foreground hover:underline">{item.title}</a>
|
|
59
81
|
{:else}
|
|
60
82
|
<span class="truncate text-body font-semibold text-foreground">{item.title}</span>
|
|
61
83
|
{/if}
|
|
62
84
|
{#if item.featured}<Chip size="sm" variant="warning" label="Staff pick" />{/if}
|
|
63
85
|
</span>
|
|
64
86
|
<span class="truncate text-footnote text-muted-foreground">{item.desc}</span>
|
|
87
|
+
<CreatorLine creator={item.creator} class="mt-0.5" />
|
|
65
88
|
</div>
|
|
66
|
-
<div class="hidden w-40 shrink-0 text-caption-1 text-muted-foreground md:flex">
|
|
67
|
-
|
|
89
|
+
<div class="hidden w-40 shrink-0 flex-col gap-0.5 text-caption-1 text-muted-foreground md:flex" data-testid="discover-meta">
|
|
90
|
+
{#if showUsage(item)}
|
|
91
|
+
<span class="inline-flex items-center gap-1 tabular-nums"><GitFork class="size-3" aria-hidden="true" />{usageLabel(item)}</span>
|
|
92
|
+
{/if}
|
|
93
|
+
{#if published}<span class="tabular-nums">{published}</span>{/if}
|
|
68
94
|
</div>
|
|
69
|
-
<ItemActions {item} {onremix} {onreport} {onopen} {busy} />
|
|
95
|
+
<ItemActions {item} pageHref={href} {remixHref} {onremix} {onreport} {onopen} {oncopylink} {busy} />
|
|
70
96
|
</div>
|
|
@@ -6,9 +6,16 @@ type $$ComponentProps = {
|
|
|
6
6
|
/** Passed to both links. Off-site consumers pass target="_blank" rel="noopener". */
|
|
7
7
|
target?: string;
|
|
8
8
|
rel?: string;
|
|
9
|
+
/** Claim / Use this recipe and Remix as links (the website, to the app). Wins over `onremix`. */
|
|
10
|
+
remixHref?: string;
|
|
9
11
|
onremix?: (item: DiscoverItem) => void;
|
|
10
12
|
onreport?: (item: DiscoverItem) => void;
|
|
13
|
+
/** Opens the item in place. With `href`, a plain click on the art or title calls
|
|
14
|
+
* it instead of following the link; modified clicks and JS-off still navigate.
|
|
15
|
+
* For studio items it also drives the Play / View button. */
|
|
11
16
|
onopen?: (item: DiscoverItem) => void;
|
|
17
|
+
/** Copy link in the ⋯ menu, shown with `href`. Gets the item and that href. */
|
|
18
|
+
oncopylink?: (item: DiscoverItem, href: string) => void;
|
|
12
19
|
busy?: boolean;
|
|
13
20
|
class?: string;
|
|
14
21
|
};
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
heading and its blurb, an optional "see all" link, then the items in a
|
|
3
3
|
DiscoverGrid for the given view. Children are the tiles (or rows, for the
|
|
4
4
|
list view). The "see all" control is a link with `moreHref` and a button
|
|
5
|
-
with `onmore`; with neither it is not drawn.
|
|
5
|
+
with `onmore`; with neither it is not drawn. It sits on the title's row,
|
|
6
|
+
right-aligned, and wraps under the title (still right-aligned) only when
|
|
7
|
+
the row is too narrow; the blurb runs under both. It paints text-primary,
|
|
8
|
+
the host's own accent, which reads on the app's and the public site's
|
|
9
|
+
surfaces alike (the link button's ripple accent token is the light set's
|
|
10
|
+
dark blue on a host that only defines its own tokens). Ids follow the app:
|
|
6
11
|
discover-h-{id} on the heading, discover-section-{id} as the test id. -->
|
|
7
12
|
<script lang="ts">
|
|
8
13
|
import { safeUrl } from '@ripple-ui/core';
|
|
@@ -35,19 +40,22 @@
|
|
|
35
40
|
class?: string;
|
|
36
41
|
children: Snippet;
|
|
37
42
|
} = $props();
|
|
43
|
+
|
|
44
|
+
// No side padding (the label lines up with the grid's edge), the host accent.
|
|
45
|
+
const moreClass = 'ml-auto h-auto px-0 text-primary';
|
|
38
46
|
</script>
|
|
39
47
|
|
|
40
48
|
<section class={cn('flex flex-col gap-4', className)} aria-labelledby={`discover-h-${id}`} data-testid={`discover-section-${id}`}>
|
|
41
|
-
<div class="flex flex-
|
|
42
|
-
<div>
|
|
43
|
-
<h2 id={`discover-h-${id}`} class="text-title-3 font-semibold text-foreground">{title}</h2>
|
|
44
|
-
{#if
|
|
49
|
+
<div class="flex flex-col gap-0.5">
|
|
50
|
+
<div class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
|
|
51
|
+
<h2 id={`discover-h-${id}`} class="min-w-0 text-title-3 font-semibold text-foreground">{title}</h2>
|
|
52
|
+
{#if more && moreHref}
|
|
53
|
+
<Button variant="link" size="sm" class={moreClass} href={safeUrl(moreHref)} data-testid={`discover-more-${id}`}>{more} →</Button>
|
|
54
|
+
{:else if more && onmore}
|
|
55
|
+
<Button variant="link" size="sm" class={moreClass} onclick={onmore} data-testid={`discover-more-${id}`}>{more} →</Button>
|
|
56
|
+
{/if}
|
|
45
57
|
</div>
|
|
46
|
-
{#if
|
|
47
|
-
<Button variant="link" size="sm" href={safeUrl(moreHref)}>{more} →</Button>
|
|
48
|
-
{:else if more && onmore}
|
|
49
|
-
<Button variant="link" size="sm" onclick={onmore}>{more} →</Button>
|
|
50
|
-
{/if}
|
|
58
|
+
{#if blurb}<p class="m-0 text-subheadline text-muted-foreground">{blurb}</p>{/if}
|
|
51
59
|
</div>
|
|
52
60
|
<DiscoverGrid {view} label={title}>{@render children()}</DiscoverGrid>
|
|
53
61
|
</section>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<!-- discover/DiscoverSkeleton.svelte — the Discover loading state in the exact
|
|
2
2
|
geometry of what replaces it: skeleton rows in the bordered list for the list
|
|
3
3
|
view, tile skeletons (16:9 picture, title bar, line bar) in the grid
|
|
4
|
-
otherwise
|
|
4
|
+
otherwise; under 15rem a tile skeleton takes the compact tile's shape
|
|
5
|
+
(title bar, then a small action pill instead of the line). One role="status" wrapper carries the label; the shapes are
|
|
5
6
|
decorative. Classes are paw-enterprise's Skeleton and TemplateTileSkeleton. -->
|
|
6
7
|
<script lang="ts">
|
|
7
8
|
import { cn } from '../utils.js';
|
|
@@ -39,10 +40,11 @@
|
|
|
39
40
|
{:else}
|
|
40
41
|
<div class={GRID_CLASS}>
|
|
41
42
|
{#each slots as i (i)}
|
|
42
|
-
<div class="flex flex-col gap-3" aria-hidden="true">
|
|
43
|
+
<div class="@container/tile flex flex-col gap-3 @max-[15rem]/tile:gap-2" aria-hidden="true">
|
|
43
44
|
<div data-slot="skeleton" class={cn(bone, 'aspect-video w-full rounded-2xl')}></div>
|
|
44
45
|
<div data-slot="skeleton" class={cn(bone, 'h-4 w-2/3 rounded-md')}></div>
|
|
45
|
-
<div data-slot="skeleton" class={cn(bone, 'h-3 w-full rounded-md')}></div>
|
|
46
|
+
<div data-slot="skeleton" class={cn(bone, 'h-3 w-full rounded-md @max-[15rem]/tile:hidden')}></div>
|
|
47
|
+
<div data-slot="skeleton" class={cn(bone, 'hidden h-7 w-16 rounded-full @max-[15rem]/tile:block')}></div>
|
|
46
48
|
</div>
|
|
47
49
|
{/each}
|
|
48
50
|
</div>
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
<!-- discover/DiscoverTile.svelte — one Discover item in a grid or shelf: the
|
|
2
|
-
picture as a borderless rounded tile, then title, one line
|
|
3
|
-
line,
|
|
2
|
+
picture as a borderless rounded tile, then title, one line, the creator
|
|
3
|
+
line (avatar and "by <name>", only when the listing has a creator) and a
|
|
4
|
+
quiet meta line (usage, left out at zero, and the published date), with
|
|
5
|
+
the actions beside them.
|
|
6
|
+
|
|
7
|
+
The tile sizes off its own width (container `tile`), since the same tile
|
|
8
|
+
sits in a two-column phone grid, a shelf and a related row. Under 15rem it
|
|
9
|
+
goes compact: the title clamps to two lines, the line and the meta line
|
|
10
|
+
hide, and the actions drop under the text as a small primary button and
|
|
11
|
+
the ⋯ menu (the Remix icon hides there; Remix is in ⋯). The type scale (text-headline,
|
|
4
12
|
text-footnote, text-caption-1), the 10px picture radius and the hover lift
|
|
5
|
-
match paw-enterprise's TemplateTile, the app's own tile.
|
|
6
|
-
art and the title are links; the art
|
|
7
|
-
the name). `target` and `rel` pass
|
|
8
|
-
item pages are off-site.
|
|
13
|
+
match paw-enterprise's TemplateTile, the app's own tile. The usage line is
|
|
14
|
+
left out at zero. With `href`, the art and the title are links; the art
|
|
15
|
+
link is decorative (the title carries the name). `target` and `rel` pass
|
|
16
|
+
through to both, for consumers whose item pages are off-site. With
|
|
17
|
+
`onopen` too, a plain click on either opens the item in place (an
|
|
18
|
+
overlay) and a modified click still follows the link. Renders fully on
|
|
19
|
+
the server. -->
|
|
9
20
|
<script lang="ts">
|
|
10
21
|
import { safeUrl } from '@ripple-ui/core';
|
|
11
22
|
import GitFork from '@lucide/svelte/icons/git-fork';
|
|
@@ -14,7 +25,9 @@
|
|
|
14
25
|
import ItemActions from './ItemActions.svelte';
|
|
15
26
|
import ItemArt from './ItemArt.svelte';
|
|
16
27
|
import { initialFor, tintFor } from './art.js';
|
|
17
|
-
import {
|
|
28
|
+
import { isPlainClick } from './href.js';
|
|
29
|
+
import { publishedDate, showUsage, usageLabel } from './item.js';
|
|
30
|
+
import CreatorLine from './CreatorLine.svelte';
|
|
18
31
|
import type { DiscoverItem } from './types.js';
|
|
19
32
|
|
|
20
33
|
let {
|
|
@@ -22,9 +35,11 @@
|
|
|
22
35
|
href,
|
|
23
36
|
target,
|
|
24
37
|
rel,
|
|
38
|
+
remixHref,
|
|
25
39
|
onremix,
|
|
26
40
|
onreport,
|
|
27
41
|
onopen,
|
|
42
|
+
oncopylink,
|
|
28
43
|
busy = false,
|
|
29
44
|
class: className,
|
|
30
45
|
}: {
|
|
@@ -34,15 +49,30 @@
|
|
|
34
49
|
/** Passed to both links. Off-site consumers pass target="_blank" rel="noopener". */
|
|
35
50
|
target?: string;
|
|
36
51
|
rel?: string;
|
|
52
|
+
/** Claim / Use this recipe and Remix as links (the website, to the app). Wins over `onremix`. */
|
|
53
|
+
remixHref?: string;
|
|
37
54
|
onremix?: (item: DiscoverItem) => void;
|
|
38
55
|
onreport?: (item: DiscoverItem) => void;
|
|
56
|
+
/** Opens the item in place. With `href`, a plain click on the art or title calls
|
|
57
|
+
* it instead of following the link; modified clicks and JS-off still navigate.
|
|
58
|
+
* For studio items it also drives the Play / View button. */
|
|
39
59
|
onopen?: (item: DiscoverItem) => void;
|
|
60
|
+
/** Copy link in the ⋯ menu, shown with `href`. Gets the item and that href. */
|
|
61
|
+
oncopylink?: (item: DiscoverItem, href: string) => void;
|
|
40
62
|
busy?: boolean;
|
|
41
63
|
class?: string;
|
|
42
64
|
} = $props();
|
|
65
|
+
|
|
66
|
+
const published = $derived(publishedDate(item));
|
|
67
|
+
|
|
68
|
+
function openInPlace(e: MouseEvent) {
|
|
69
|
+
if (!onopen || !isPlainClick(e)) return;
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
onopen(item);
|
|
72
|
+
}
|
|
43
73
|
</script>
|
|
44
74
|
|
|
45
|
-
<article class={cn('group/tile flex min-w-0 flex-col gap-3', className)} data-testid={`discover-item-${item.id}`}>
|
|
75
|
+
<article class={cn('group/tile @container/tile flex min-w-0 flex-col gap-3 @max-[15rem]/tile:gap-2', className)} data-testid={`discover-item-${item.id}`}>
|
|
46
76
|
<div
|
|
47
77
|
class="relative overflow-hidden rounded-2xl transition-transform duration-150 ease-out group-focus-within/tile:-translate-y-px group-hover/tile:-translate-y-px motion-reduce:transition-none motion-reduce:group-focus-within/tile:translate-y-0 motion-reduce:group-hover/tile:translate-y-0"
|
|
48
78
|
>
|
|
@@ -50,7 +80,7 @@
|
|
|
50
80
|
<ItemArt imageUrl={item.imageUrl} title={item.title} mediaKind={item.mediaKind} tint={tintFor(item.id)} initial={initialFor(item.title)} />
|
|
51
81
|
{/snippet}
|
|
52
82
|
{#if href}
|
|
53
|
-
<a href={safeUrl(href)} {target} {rel} tabindex="-1" aria-hidden="true" class="block">{@render art()}</a>
|
|
83
|
+
<a href={safeUrl(href)} {target} {rel} onclick={openInPlace} tabindex="-1" aria-hidden="true" class="block">{@render art()}</a>
|
|
54
84
|
{:else}
|
|
55
85
|
{@render art()}
|
|
56
86
|
{/if}
|
|
@@ -58,18 +88,23 @@
|
|
|
58
88
|
<Chip size="sm" variant="warning" label="Staff pick" class="absolute top-2 left-2" />
|
|
59
89
|
{/if}
|
|
60
90
|
</div>
|
|
61
|
-
<div class="flex items-start gap-3">
|
|
91
|
+
<div class="flex items-start gap-3 @max-[15rem]/tile:flex-col @max-[15rem]/tile:items-stretch @max-[15rem]/tile:gap-2">
|
|
62
92
|
<div class="flex min-w-0 flex-1 flex-col">
|
|
63
|
-
<h3 class="m-0 truncate text-headline text-foreground">
|
|
64
|
-
{#if href}<a href={safeUrl(href)} {target} {rel} class="hover:underline">{item.title}</a>{:else}{item.title}{/if}
|
|
93
|
+
<h3 class="m-0 truncate text-headline text-foreground @max-[15rem]/tile:line-clamp-2 @max-[15rem]/tile:whitespace-normal">
|
|
94
|
+
{#if href}<a href={safeUrl(href)} {target} {rel} onclick={openInPlace} class="hover:underline">{item.title}</a>{:else}{item.title}{/if}
|
|
65
95
|
</h3>
|
|
66
96
|
{#if item.desc}
|
|
67
|
-
<p class="m-0 truncate text-footnote text-muted-foreground">{item.desc}</p>
|
|
97
|
+
<p class="m-0 truncate text-footnote text-muted-foreground @max-[15rem]/tile:hidden">{item.desc}</p>
|
|
98
|
+
{/if}
|
|
99
|
+
<CreatorLine creator={item.creator} class="mt-1" />
|
|
100
|
+
{#if showUsage(item) || published}
|
|
101
|
+
<p class="m-0 mt-1 flex flex-wrap items-center gap-x-1.5 text-caption-1 text-muted-foreground tabular-nums @max-[15rem]/tile:hidden" data-testid="discover-meta">
|
|
102
|
+
{#if showUsage(item)}<span class="inline-flex items-center gap-1.5 whitespace-nowrap"><GitFork class="size-3" aria-hidden="true" />{usageLabel(item)}</span>{/if}
|
|
103
|
+
{#if showUsage(item) && published}<span aria-hidden="true">·</span>{/if}
|
|
104
|
+
{#if published}<span class="whitespace-nowrap">{published}</span>{/if}
|
|
105
|
+
</p>
|
|
68
106
|
{/if}
|
|
69
|
-
<p class="m-0 mt-1 flex items-center gap-1.5 text-caption-1 text-muted-foreground tabular-nums">
|
|
70
|
-
<GitFork class="size-3" aria-hidden="true" />{usageLabel(item)}
|
|
71
|
-
</p>
|
|
72
107
|
</div>
|
|
73
|
-
<ItemActions {item} {onremix} {onreport} {onopen} {busy} />
|
|
108
|
+
<ItemActions {item} pageHref={href} {remixHref} {onremix} {onreport} {onopen} {oncopylink} {busy} class="@max-[15rem]/tile:justify-between" />
|
|
74
109
|
</div>
|
|
75
110
|
</article>
|