svelte-stageshow 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/NOTICE.md +32 -0
- package/README.md +34 -2
- package/dist/Avatar.svelte +57 -0
- package/dist/Avatar.svelte.d.ts +20 -0
- package/dist/Bar.svelte +27 -0
- package/dist/Bar.svelte.d.ts +18 -0
- package/dist/Bubble.svelte +255 -0
- package/dist/Bubble.svelte.d.ts +37 -0
- package/dist/Caret.svelte +10 -0
- package/dist/Caret.svelte.d.ts +7 -0
- package/dist/Chart.svelte +183 -0
- package/dist/Chart.svelte.d.ts +6 -0
- package/dist/Chip.svelte +44 -0
- package/dist/Chip.svelte.d.ts +16 -0
- package/dist/Code.svelte +95 -0
- package/dist/Code.svelte.d.ts +15 -0
- package/dist/Cursor.svelte +89 -0
- package/dist/Cursor.svelte.d.ts +13 -0
- package/dist/Keyboard.svelte +180 -0
- package/dist/Keyboard.svelte.d.ts +22 -0
- package/dist/Keycaps.svelte +20 -0
- package/dist/Keycaps.svelte.d.ts +8 -0
- package/dist/LockScreen.svelte +75 -0
- package/dist/LockScreen.svelte.d.ts +22 -0
- package/dist/Notification.svelte +38 -0
- package/dist/Notification.svelte.d.ts +16 -0
- package/dist/Phone.svelte +215 -0
- package/dist/Phone.svelte.d.ts +25 -0
- package/dist/Prompt.svelte +23 -0
- package/dist/Prompt.svelte.d.ts +13 -0
- package/dist/Receipt.svelte +24 -0
- package/dist/Receipt.svelte.d.ts +11 -0
- package/dist/Row.svelte +90 -0
- package/dist/Row.svelte.d.ts +28 -0
- package/dist/Stage.svelte +645 -0
- package/dist/Stage.svelte.d.ts +86 -0
- package/dist/Stamp.svelte +16 -0
- package/dist/Stamp.svelte.d.ts +10 -0
- package/dist/StillProvider.svelte +15 -0
- package/dist/StillProvider.svelte.d.ts +8 -0
- package/dist/Text.svelte +42 -0
- package/dist/Text.svelte.d.ts +20 -0
- package/dist/Thread.svelte +192 -0
- package/dist/Thread.svelte.d.ts +32 -0
- package/dist/Window.svelte +255 -0
- package/dist/Window.svelte.d.ts +40 -0
- package/dist/act.d.ts +36 -0
- package/dist/act.js +369 -0
- package/dist/brands/1password-dark.svg +1 -0
- package/dist/brands/1password.svg +1 -0
- package/dist/brands/LICENSE-CRYPTO-FLAT.txt +121 -0
- package/dist/brands/LICENSE-PAYMENT-ICONS.txt +201 -0
- package/dist/brands/LICENSE-SIMPLE-ICONS.txt +30 -0
- package/dist/brands/LICENSE-SVGL.txt +21 -0
- package/dist/brands/SOURCES.md +57 -0
- package/dist/brands/amex-tile.svg +1 -0
- package/dist/brands/amex.svg +1 -0
- package/dist/brands/bankofamerica-dark.svg +1 -0
- package/dist/brands/bankofamerica.svg +1 -0
- package/dist/brands/barclays.svg +1 -0
- package/dist/brands/bitcoin.svg +1 -0
- package/dist/brands/chase.svg +1 -0
- package/dist/brands/discover.svg +1 -0
- package/dist/brands/dropbox-glyph.svg +1 -0
- package/dist/brands/dropbox.svg +1 -0
- package/dist/brands/ethereum.svg +1 -0
- package/dist/brands/figma.svg +14 -0
- package/dist/brands/github-dark.svg +3 -0
- package/dist/brands/github.svg +1 -0
- package/dist/brands/gmail.svg +11 -0
- package/dist/brands/google-calendar.svg +17 -0
- package/dist/brands/google-docs.svg +1 -0
- package/dist/brands/google-drive.svg +8 -0
- package/dist/brands/google-meet.svg +16 -0
- package/dist/brands/google-sheets.svg +55 -0
- package/dist/brands/google-slides.svg +61 -0
- package/dist/brands/hsbc.svg +1 -0
- package/dist/brands/linear-dark.svg +5 -0
- package/dist/brands/linear.svg +5 -0
- package/dist/brands/mastercard.svg +5 -0
- package/dist/brands/monzo-dark.svg +1 -0
- package/dist/brands/monzo.svg +1 -0
- package/dist/brands/notion.svg +1 -0
- package/dist/brands/paypal.svg +1 -0
- package/dist/brands/revolut-dark.svg +1 -0
- package/dist/brands/revolut.svg +1 -0
- package/dist/brands/shopify.svg +5 -0
- package/dist/brands/slack.svg +24 -0
- package/dist/brands/solana.svg +1 -0
- package/dist/brands/stripe.svg +1 -0
- package/dist/brands/tether.svg +1 -0
- package/dist/brands/usdc.svg +6 -0
- package/dist/brands/vercel-dark.svg +1 -0
- package/dist/brands/vercel.svg +1 -0
- package/dist/brands/visa.svg +3 -0
- package/dist/brands/wellsfargo.svg +1 -0
- package/dist/brands/zoom.svg +1 -0
- package/dist/brands.d.ts +296 -0
- package/dist/brands.js +215 -0
- package/dist/chart.d.ts +55 -0
- package/dist/chart.js +91 -0
- package/dist/clock.d.ts +22 -0
- package/dist/clock.js +57 -0
- package/dist/code.d.ts +15 -0
- package/dist/code.js +37 -0
- package/dist/controls/Badge.svelte +29 -0
- package/dist/controls/Badge.svelte.d.ts +15 -0
- package/dist/controls/Field.svelte +145 -0
- package/dist/controls/Field.svelte.d.ts +34 -0
- package/dist/controls/Segmented.svelte +34 -0
- package/dist/controls/Segmented.svelte.d.ts +16 -0
- package/dist/controls/Slot.svelte +34 -0
- package/dist/controls/Slot.svelte.d.ts +16 -0
- package/dist/controls/Tabs.svelte +44 -0
- package/dist/controls/Tabs.svelte.d.ts +14 -0
- package/dist/controls/Toggle.svelte +24 -0
- package/dist/controls/Toggle.svelte.d.ts +11 -0
- package/dist/cursors/MacCrosshair.svelte +5 -0
- package/dist/cursors/MacCrosshair.svelte.d.ts +18 -0
- package/dist/cursors/MacGrab.svelte +7 -0
- package/dist/cursors/MacGrab.svelte.d.ts +18 -0
- package/dist/cursors/MacIbeam.svelte +5 -0
- package/dist/cursors/MacIbeam.svelte.d.ts +18 -0
- package/dist/cursors/MacOpenHand.svelte +12 -0
- package/dist/cursors/MacOpenHand.svelte.d.ts +18 -0
- package/dist/cursors/MacPointer.svelte +7 -0
- package/dist/cursors/MacPointer.svelte.d.ts +18 -0
- package/dist/cursors/MacPointingHand.svelte +11 -0
- package/dist/cursors/MacPointingHand.svelte.d.ts +18 -0
- package/dist/cursors/MacResize.svelte +23 -0
- package/dist/cursors/MacResize.svelte.d.ts +6 -0
- package/dist/cursors/MacResizeEW.svelte +5 -0
- package/dist/cursors/MacResizeEW.svelte.d.ts +18 -0
- package/dist/cursors/MacResizeNESW.svelte +5 -0
- package/dist/cursors/MacResizeNESW.svelte.d.ts +18 -0
- package/dist/cursors/MacResizeNS.svelte +5 -0
- package/dist/cursors/MacResizeNS.svelte.d.ts +18 -0
- package/dist/cursors/MacResizeNWSE.svelte +5 -0
- package/dist/cursors/MacResizeNWSE.svelte.d.ts +18 -0
- package/dist/cursors/MacWait.svelte +5 -0
- package/dist/cursors/MacWait.svelte.d.ts +18 -0
- package/dist/cursors.d.ts +12 -0
- package/dist/cursors.js +15 -0
- package/dist/ease.d.ts +19 -0
- package/dist/ease.js +55 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +73 -0
- package/dist/ios.d.ts +1 -0
- package/dist/ios.js +2 -0
- package/dist/keys.d.ts +10 -0
- package/dist/keys.js +20 -0
- package/dist/look.d.ts +15 -0
- package/dist/look.js +14 -0
- package/dist/looks.d.ts +5 -0
- package/dist/looks.js +4 -0
- package/dist/measure.d.ts +61 -0
- package/dist/measure.js +103 -0
- package/dist/messages.d.ts +4 -0
- package/dist/messages.js +6 -0
- package/dist/motion/Camera.svelte +91 -0
- package/dist/motion/Camera.svelte.d.ts +20 -0
- package/dist/motion/Collapse.svelte +91 -0
- package/dist/motion/Collapse.svelte.d.ts +31 -0
- package/dist/motion/Connector.svelte +242 -0
- package/dist/motion/Connector.svelte.d.ts +34 -0
- package/dist/motion/Count.svelte +170 -0
- package/dist/motion/Count.svelte.d.ts +17 -0
- package/dist/motion/Expand.svelte +148 -0
- package/dist/motion/Expand.svelte.d.ts +18 -0
- package/dist/motion/Fly.svelte +100 -0
- package/dist/motion/Fly.svelte.d.ts +22 -0
- package/dist/motion/Follow.svelte +185 -0
- package/dist/motion/Follow.svelte.d.ts +23 -0
- package/dist/motion/Popover.svelte +80 -0
- package/dist/motion/Popover.svelte.d.ts +16 -0
- package/dist/motion/Roll.svelte +236 -0
- package/dist/motion/Roll.svelte.d.ts +24 -0
- package/dist/motion/Scroll.svelte +53 -0
- package/dist/motion/Scroll.svelte.d.ts +14 -0
- package/dist/motion/States.svelte +50 -0
- package/dist/motion/States.svelte.d.ts +35 -0
- package/dist/motion/Stream.svelte +28 -0
- package/dist/motion/Stream.svelte.d.ts +13 -0
- package/dist/motion/Swap.svelte +149 -0
- package/dist/motion/Swap.svelte.d.ts +14 -0
- package/dist/parts/BrandMark.svelte +59 -0
- package/dist/parts/BrandMark.svelte.d.ts +19 -0
- package/dist/parts/Check.svelte +32 -0
- package/dist/parts/Check.svelte.d.ts +13 -0
- package/dist/parts/Facepile.svelte +39 -0
- package/dist/parts/Facepile.svelte.d.ts +16 -0
- package/dist/parts/Icon.svelte +225 -0
- package/dist/parts/Icon.svelte.d.ts +267 -0
- package/dist/parts/Menu.svelte +29 -0
- package/dist/parts/Menu.svelte.d.ts +14 -0
- package/dist/parts/Meter.svelte +27 -0
- package/dist/parts/Meter.svelte.d.ts +12 -0
- package/dist/parts/Pin.svelte +44 -0
- package/dist/parts/Pin.svelte.d.ts +20 -0
- package/dist/parts/Spinner.svelte +26 -0
- package/dist/parts/Spinner.svelte.d.ts +11 -0
- package/dist/parts/Toast.svelte +47 -0
- package/dist/parts/Toast.svelte.d.ts +15 -0
- package/dist/parts/Tooltip.svelte +34 -0
- package/dist/parts/Tooltip.svelte.d.ts +17 -0
- package/dist/reduced-motion.d.ts +8 -0
- package/dist/reduced-motion.js +37 -0
- package/dist/stageshow.css +4754 -0
- package/dist/still.d.ts +2 -0
- package/dist/still.js +10 -0
- package/dist/style/avatar.css +102 -0
- package/dist/style/chart.css +59 -0
- package/dist/style/chip.css +66 -0
- package/dist/style/code.css +87 -0
- package/dist/style/controls/Badge.css +56 -0
- package/dist/style/controls/Field.css +201 -0
- package/dist/style/controls/Segmented.css +40 -0
- package/dist/style/controls/Slot.css +39 -0
- package/dist/style/controls/Tabs.css +33 -0
- package/dist/style/controls/Toggle.css +46 -0
- package/dist/style/cursor.css +256 -0
- package/dist/style/ios.css +177 -0
- package/dist/style/keys.css +60 -0
- package/dist/style/messages.css +462 -0
- package/dist/style/motion/Camera.css +27 -0
- package/dist/style/motion/Collapse.css +97 -0
- package/dist/style/motion/Connector.css +128 -0
- package/dist/style/motion/Count.css +68 -0
- package/dist/style/motion/Expand.css +80 -0
- package/dist/style/motion/Fly.css +51 -0
- package/dist/style/motion/Follow.css +17 -0
- package/dist/style/motion/Popover.css +65 -0
- package/dist/style/motion/Roll.css +77 -0
- package/dist/style/motion/Scroll.css +49 -0
- package/dist/style/motion/States.css +49 -0
- package/dist/style/motion/Stream.css +32 -0
- package/dist/style/motion/Swap.css +48 -0
- package/dist/style/parts/BrandMark.css +45 -0
- package/dist/style/parts/Check.css +94 -0
- package/dist/style/parts/Facepile.css +17 -0
- package/dist/style/parts/Icon.css +8 -0
- package/dist/style/parts/Menu.css +34 -0
- package/dist/style/parts/Meter.css +46 -0
- package/dist/style/parts/Pin.css +58 -0
- package/dist/style/parts/Spinner.css +12 -0
- package/dist/style/parts/Toast.css +35 -0
- package/dist/style/parts/Tooltip.css +42 -0
- package/dist/style/phone.css +539 -0
- package/dist/style/row.css +211 -0
- package/dist/style/stage.css +448 -0
- package/dist/style/window.css +612 -0
- package/dist/types.d.ts +87 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +4 -0
- package/dist/util.js +10 -0
- package/dist/wallpaper.d.ts +3 -0
- package/dist/wallpaper.js +3 -0
- package/package.json +41 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benji Taylor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Notices
|
|
2
|
+
|
|
3
|
+
Stageshow source is MIT licensed; see `LICENSE`.
|
|
4
|
+
|
|
5
|
+
The Svelte 5 port in this repository (`svelte-stageshow`, 2026) is by Ammar
|
|
6
|
+
(maven-ammar). It keeps the original MIT licence and this notice. The ported
|
|
7
|
+
components and the two gallery scenes come from the public Stageshow source in
|
|
8
|
+
`benjitaylor/skills`; the original gold stills were filmed from that source and
|
|
9
|
+
are kept in `shots/` for the parity record in the repository's `REHEARSAL.md`.
|
|
10
|
+
|
|
11
|
+
Product and operating-system names, familiar device geometry, app icons, and
|
|
12
|
+
illustrated interfaces are used as examples. Those names and marks remain
|
|
13
|
+
with their owners; the Stageshow license does not grant trademark rights or
|
|
14
|
+
imply endorsement. Adapt examples to the product you are illustrating.
|
|
15
|
+
|
|
16
|
+
The Radix-inspired look uses publicly documented color and spacing values;
|
|
17
|
+
its source is identified in `examples/looks/radix.css`. The app icons in `assets/app-icons.ts` are artwork from their respective app
|
|
18
|
+
bundles, as identified in that file; they are not covered by the Stageshow MIT
|
|
19
|
+
license. They remain illustrative references to those products. The Wikipedia
|
|
20
|
+
study includes two local photographs dedicated under CC0 1.0 (also used
|
|
21
|
+
in the Finder, Telegram and annotation studies); their authors,
|
|
22
|
+
original source pages and modifications are recorded in
|
|
23
|
+
[`examples/wikipedia/PHOTOS.md`](examples/wikipedia/PHOTOS.md). Other scenes
|
|
24
|
+
use CSS and vector illustrations. No third-party fonts are bundled, and the
|
|
25
|
+
playground does not fetch fonts or photographs from external services at runtime.
|
|
26
|
+
|
|
27
|
+
The local brand registry in `kit/brands/` includes SVG artwork from SVGL (MIT), payment-icons (Apache 2.0), Cryptocurrency Icons (CC0 1.0), and official Linear, American Express and Circle assets. Per-file sources, hashes and license files are listed in [`kit/brands/SOURCES.md`](kit/brands/SOURCES.md). The original artwork and brand usage terms remain with their respective owners.
|
|
28
|
+
|
|
29
|
+
`Phone frame="photo"` is optional. Supply your own permitted bezel image via
|
|
30
|
+
`--ss-phone-photo-light: url(...)` and optionally `--ss-phone-photo-dark`.
|
|
31
|
+
Without it, the kit uses its CSS-drawn `pro` frame. No bezel photographs are
|
|
32
|
+
bundled.
|
package/README.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
-
#
|
|
1
|
+
# svelte-stageshow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Svelte 5 port of Stageshow: framed faux product UI for stills and scripted
|
|
4
|
+
demos. Stage, Window, Bar, Text, Row, Phone, Avatar and Chip; one CSS import;
|
|
5
|
+
an Act scheduler with deterministic frame stepping.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add svelte-stageshow
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```svelte
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
import { Avatar, Row, Stage, Window } from 'svelte-stageshow'
|
|
14
|
+
import 'svelte-stageshow/style.css'
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<Stage width={640} height={320} className="my-stage">
|
|
18
|
+
{#snippet children(state, rect, layout)}
|
|
19
|
+
<Window title="Customers" className="my-window">
|
|
20
|
+
<Row selected title="Polaris Studio" subtitle="Scale · 24 seats">
|
|
21
|
+
{#snippet lead()}<Avatar name="Polaris Studio" size={30} tone="accent" />{/snippet}
|
|
22
|
+
{#snippet meta()}$12,400.00{/snippet}
|
|
23
|
+
</Row>
|
|
24
|
+
</Window>
|
|
25
|
+
{/snippet}
|
|
26
|
+
</Stage>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The full README in the repository has the prop mapping (72 React prop names,
|
|
30
|
+
0 missing), the diff table, the deterministic `runFrames()` contract and the
|
|
31
|
+
verification record (`REHEARSAL.md`).
|
|
32
|
+
|
|
33
|
+
- Intended repository: `maven-ammar/svelte-stageshow` (private, not pushed yet)
|
|
34
|
+
- Licence: MIT, © 2026 Benji Taylor; Svelte port by Ammar (maven-ammar), 2026
|
|
35
|
+
- Peer: `svelte` ^5. No React, react-dom or icon dependency.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
3
|
+
import { getLook } from './look.js'
|
|
4
|
+
|
|
5
|
+
type Tone = 'accent' | 'ink' | 'success' | 'person' | 'ios'
|
|
6
|
+
type Ring = 'pending' | 'waiting' | 'confirmed' | 'declined'
|
|
7
|
+
|
|
8
|
+
interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, 'class'> {
|
|
9
|
+
/** Its initial. Leave it out (or set `plain`) for a plain disc. */
|
|
10
|
+
name?: string
|
|
11
|
+
/** A plain disc in any fidelity: filler people, so they don't compete with the named one. */
|
|
12
|
+
plain?: boolean
|
|
13
|
+
size?: number
|
|
14
|
+
tone?: Tone
|
|
15
|
+
src?: string
|
|
16
|
+
ring?: Ring
|
|
17
|
+
className?: string
|
|
18
|
+
/** Svelte's own class attribute; merged after className. */
|
|
19
|
+
class?: string
|
|
20
|
+
style?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let { name = '', plain = false, size = 24, tone, src, ring, className = '', class: klass = '', style = '', ...rest }: Props = $props()
|
|
24
|
+
|
|
25
|
+
// Six muted, opaque fills that read on light and dark grounds, one per person
|
|
26
|
+
const PEOPLE = ['#c98f6e', '#6f9b7d', '#7b86c7', '#c47a9e', '#8c9ab0', '#c2a35a']
|
|
27
|
+
const hue = (value: string) => {
|
|
28
|
+
let h = 0
|
|
29
|
+
for (const c of value) h = (h * 31 + c.charCodeAt(0)) % 9973
|
|
30
|
+
return PEOPLE[h % PEOPLE.length]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const look = getLook()
|
|
34
|
+
const disc = $derived(plain || look.fidelity === 'minimal')
|
|
35
|
+
const toneValue: Tone = $derived(tone ?? (name ? 'person' : 'ink'))
|
|
36
|
+
const person = $derived(toneValue === 'person' && !disc ? `background:${hue(name)};color:#fff;` : '')
|
|
37
|
+
const words = $derived(name.trim().split(/\s+/).filter(Boolean))
|
|
38
|
+
const initials = $derived(
|
|
39
|
+
toneValue === 'ios' ? (words[0]?.[0] ?? '') + (words.length > 1 ? words[words.length - 1]![0] : '') : (name.trim()[0] ?? ''),
|
|
40
|
+
)
|
|
41
|
+
const fontSize = $derived(Math.round(size * (toneValue === 'ios' ? 0.4 : size < 24 ? 0.5 : 0.42)))
|
|
42
|
+
const css = $derived(`width:${size}px;height:${size}px;font-size:${fontSize}px;${person}${style}`)
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<span
|
|
46
|
+
{...rest}
|
|
47
|
+
class={`ss-avatar${className ? ` ${className}` : ''}${klass ? ` ${klass}` : ''}`}
|
|
48
|
+
data-tone={toneValue}
|
|
49
|
+
data-ring={ring}
|
|
50
|
+
style={css}
|
|
51
|
+
>
|
|
52
|
+
{#if src && !disc}
|
|
53
|
+
<img {src} alt="" />
|
|
54
|
+
{:else if !disc}
|
|
55
|
+
<span class="ss-avatar__initial">{initials.toUpperCase()}</span>
|
|
56
|
+
{/if}
|
|
57
|
+
</span>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
type Tone = 'accent' | 'ink' | 'success' | 'person' | 'ios';
|
|
3
|
+
type Ring = 'pending' | 'waiting' | 'confirmed' | 'declined';
|
|
4
|
+
interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, 'class'> {
|
|
5
|
+
/** Its initial. Leave it out (or set `plain`) for a plain disc. */
|
|
6
|
+
name?: string;
|
|
7
|
+
/** A plain disc in any fidelity: filler people, so they don't compete with the named one. */
|
|
8
|
+
plain?: boolean;
|
|
9
|
+
size?: number;
|
|
10
|
+
tone?: Tone;
|
|
11
|
+
src?: string;
|
|
12
|
+
ring?: Ring;
|
|
13
|
+
className?: string;
|
|
14
|
+
/** Svelte's own class attribute; merged after className. */
|
|
15
|
+
class?: string;
|
|
16
|
+
style?: string;
|
|
17
|
+
}
|
|
18
|
+
declare const Avatar: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type Avatar = ReturnType<typeof Avatar>;
|
|
20
|
+
export default Avatar;
|
package/dist/Bar.svelte
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
3
|
+
|
|
4
|
+
interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, 'class'> {
|
|
5
|
+
/** Width in px, or any CSS length. */
|
|
6
|
+
w: number | string
|
|
7
|
+
h?: number
|
|
8
|
+
/** 1 (faintest) to 5. */
|
|
9
|
+
tone?: 1 | 2 | 3 | 4 | 5
|
|
10
|
+
r?: number
|
|
11
|
+
/** Let it narrow in a row too tight for it. On by default for a line. */
|
|
12
|
+
shrink?: boolean
|
|
13
|
+
className?: string
|
|
14
|
+
/** Svelte's own class attribute; merged after className. */
|
|
15
|
+
class?: string
|
|
16
|
+
style?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let { w, h = 6, tone = 3, r = 2, shrink, className = '', class: klass = '', style = '', ...rest }: Props = $props()
|
|
20
|
+
|
|
21
|
+
const INK = ['var(--ss-ink-1)', 'var(--ss-ink-2)', 'var(--ss-ink-3)', 'var(--ss-ink-4)', 'var(--ss-ink-5)'] as const
|
|
22
|
+
const gives = $derived(shrink ?? (typeof w === 'number' && w > h * 3))
|
|
23
|
+
const width = $derived(typeof w === 'number' ? `${w}px` : w)
|
|
24
|
+
const css = $derived(`width:${width};height:${h}px;border-radius:${r}px;background:${INK[tone - 1]};${style}`)
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<span {...rest} class={`ss-bar${className ? ` ${className}` : ''}${klass ? ` ${klass}` : ''}`} data-shrink={gives || undefined} style={css}></span>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, 'class'> {
|
|
3
|
+
/** Width in px, or any CSS length. */
|
|
4
|
+
w: number | string;
|
|
5
|
+
h?: number;
|
|
6
|
+
/** 1 (faintest) to 5. */
|
|
7
|
+
tone?: 1 | 2 | 3 | 4 | 5;
|
|
8
|
+
r?: number;
|
|
9
|
+
/** Let it narrow in a row too tight for it. On by default for a line. */
|
|
10
|
+
shrink?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
/** Svelte's own class attribute; merged after className. */
|
|
13
|
+
class?: string;
|
|
14
|
+
style?: string;
|
|
15
|
+
}
|
|
16
|
+
declare const Bar: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type Bar = ReturnType<typeof Bar>;
|
|
18
|
+
export default Bar;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext, onDestroy, onMount, untrack } from 'svelte'
|
|
3
|
+
import type { Snippet } from 'svelte'
|
|
4
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
5
|
+
import { getLook } from './look.js'
|
|
6
|
+
import { prefersReducedMotion, reducedMotion } from './reduced-motion.js'
|
|
7
|
+
import { THREAD_KEY, type ThreadContext } from './Thread.svelte'
|
|
8
|
+
|
|
9
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'class' | 'children'> {
|
|
10
|
+
/** me sits on the right in the accent bubble; them on the left. */
|
|
11
|
+
from: 'me' | 'them'
|
|
12
|
+
/** Three bouncing dots in place of the text. Turning it off grows the bubble into the message. */
|
|
13
|
+
typing?: boolean
|
|
14
|
+
/** Set by Thread: true on the last bubble of a run. */
|
|
15
|
+
tail?: boolean
|
|
16
|
+
/**
|
|
17
|
+
* Where the message comes from, as iMessage sends: a target name (or a
|
|
18
|
+
* CSS selector) for the composer's text. The bubble forms out of it, the
|
|
19
|
+
* words lifting from the field into place while the bubble fills in around
|
|
20
|
+
* them. Clear the field in the same update that adds the bubble.
|
|
21
|
+
*/
|
|
22
|
+
source?: string
|
|
23
|
+
/**
|
|
24
|
+
* A tapback on this bubble: the emoji, and `show` to pop it in and out.
|
|
25
|
+
* It sits in a white circle on the bubble's bottom corner, and the bubble
|
|
26
|
+
* makes room below so the emoji never covers the words.
|
|
27
|
+
*/
|
|
28
|
+
tapback?: { emoji: Snippet | string; show?: boolean }
|
|
29
|
+
/** A small `Avatar` beside a received bubble, as a group chat shows who is speaking. */
|
|
30
|
+
avatar?: Snippet
|
|
31
|
+
className?: string
|
|
32
|
+
/** Svelte's own class attribute; merged after className. */
|
|
33
|
+
class?: string
|
|
34
|
+
style?: string
|
|
35
|
+
/** Text, or a snippet for rich content. A string can also be passed to `children` directly. */
|
|
36
|
+
children?: Snippet | string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let {
|
|
40
|
+
avatar,
|
|
41
|
+
from,
|
|
42
|
+
typing = false,
|
|
43
|
+
tail,
|
|
44
|
+
source,
|
|
45
|
+
tapback,
|
|
46
|
+
className = '',
|
|
47
|
+
class: klass = '',
|
|
48
|
+
style = '',
|
|
49
|
+
children,
|
|
50
|
+
...rest
|
|
51
|
+
}: Props = $props()
|
|
52
|
+
|
|
53
|
+
const { fidelity } = getLook()
|
|
54
|
+
const thread = getContext<ThreadContext | undefined>(THREAD_KEY)
|
|
55
|
+
// Bubbles in the thread on its first render are the opening picture and
|
|
56
|
+
// don't animate; only later arrivals rise in.
|
|
57
|
+
const arrived = thread ? thread.ready : true
|
|
58
|
+
|
|
59
|
+
let rowEl = $state<HTMLDivElement | null>(null)
|
|
60
|
+
let bubbleEl = $state<HTMLDivElement | null>(null)
|
|
61
|
+
let autoTail = $state<boolean | undefined>(undefined)
|
|
62
|
+
const effectiveTail = $derived(tail !== undefined ? tail : autoTail)
|
|
63
|
+
|
|
64
|
+
onMount(() => {
|
|
65
|
+
if (!thread) return
|
|
66
|
+
return thread.register({
|
|
67
|
+
row: () => rowEl,
|
|
68
|
+
from: () => from,
|
|
69
|
+
explicit: () => tail !== undefined,
|
|
70
|
+
setTail: (value) => {
|
|
71
|
+
autoTail = value
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Whether this bubble grew out of the typing dots (its words fade in as it
|
|
77
|
+
// grows). The flag latches on, as the React ref did.
|
|
78
|
+
let lastTyping = $state(untrack(() => typing))
|
|
79
|
+
let grew = $state(false)
|
|
80
|
+
$effect.pre(() => {
|
|
81
|
+
if (lastTyping && !typing) grew = true
|
|
82
|
+
lastTyping = typing
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Morph: measure the bubble before and after the content changes, then ease
|
|
86
|
+
// its box from the old size to the new.
|
|
87
|
+
let size: { w: number; h: number } | null = null
|
|
88
|
+
let animating = false
|
|
89
|
+
let morphTimer: ReturnType<typeof setTimeout> | null = null
|
|
90
|
+
|
|
91
|
+
function measure(node: HTMLElement) {
|
|
92
|
+
const measured = getComputedStyle(node)
|
|
93
|
+
return { w: parseFloat(measured.width), h: parseFloat(measured.height) }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function morph() {
|
|
97
|
+
const el = bubbleEl
|
|
98
|
+
if (!el || animating) return
|
|
99
|
+
const was = size
|
|
100
|
+
el.style.width = ''
|
|
101
|
+
el.style.height = ''
|
|
102
|
+
// Resolved CSS dimensions retain the fractions offsetWidth/Height round
|
|
103
|
+
// away, so releasing the measured box doesn't resize it at the endpoint.
|
|
104
|
+
const now = measure(el)
|
|
105
|
+
size = now
|
|
106
|
+
if (!was || (was.w === now.w && was.h === now.h)) return
|
|
107
|
+
if (reducedMotion(el)) return
|
|
108
|
+
animating = true
|
|
109
|
+
// The words keep their final line breaks while the box grows around them
|
|
110
|
+
const text = el.querySelector<HTMLElement>('.ss-bubble__text')
|
|
111
|
+
if (text) text.style.width = getComputedStyle(text).width
|
|
112
|
+
el.style.transition = 'none'
|
|
113
|
+
el.style.width = `${was.w}px`
|
|
114
|
+
el.style.height = `${was.h}px`
|
|
115
|
+
void el.offsetWidth
|
|
116
|
+
el.style.transition = ''
|
|
117
|
+
el.style.width = `${now.w}px`
|
|
118
|
+
el.style.height = `${now.h}px`
|
|
119
|
+
const end = () => {
|
|
120
|
+
if (morphTimer) {
|
|
121
|
+
clearTimeout(morphTimer)
|
|
122
|
+
morphTimer = null
|
|
123
|
+
}
|
|
124
|
+
el.removeEventListener('transitionend', end)
|
|
125
|
+
el.style.width = ''
|
|
126
|
+
el.style.height = ''
|
|
127
|
+
if (text) text.style.width = ''
|
|
128
|
+
animating = false
|
|
129
|
+
size = measure(el)
|
|
130
|
+
}
|
|
131
|
+
el.addEventListener('transitionend', end, { once: true })
|
|
132
|
+
// Reduced motion or a stage still can remove the transition entirely, so a
|
|
133
|
+
// timer releases the measured box even if transitionend never arrives.
|
|
134
|
+
morphTimer = setTimeout(end, 600)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
$effect(() => {
|
|
138
|
+
void typing
|
|
139
|
+
void children
|
|
140
|
+
morph()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
$effect(() => {
|
|
144
|
+
const el = bubbleEl
|
|
145
|
+
if (!el) return
|
|
146
|
+
size = measure(el)
|
|
147
|
+
const ro = new ResizeObserver(() => morph())
|
|
148
|
+
ro.observe(el)
|
|
149
|
+
return () => ro.disconnect()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
onDestroy(() => {
|
|
153
|
+
if (morphTimer) clearTimeout(morphTimer)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// Form out of the composer: start where the field's words were, in the
|
|
157
|
+
// field's color with no fill, and travel into place as the fill comes in.
|
|
158
|
+
$effect(() => {
|
|
159
|
+
const el = bubbleEl
|
|
160
|
+
if (!el || !arrived || !source) return
|
|
161
|
+
const stage = el.closest('.ss-stage') ?? document
|
|
162
|
+
const src =
|
|
163
|
+
stage.querySelector<HTMLElement>(`[data-target="${source}"]`) ?? stage.querySelector<HTMLElement>(source)
|
|
164
|
+
const text = el.querySelector<HTMLElement>('.ss-bubble__text')
|
|
165
|
+
if (!src || !text) return
|
|
166
|
+
if (prefersReducedMotion() || el.closest('[data-still]')) return
|
|
167
|
+
// Measure where it rests, not where an earlier run already moved it
|
|
168
|
+
el.style.transition = 'none'
|
|
169
|
+
el.style.left = el.style.top = '0px'
|
|
170
|
+
const k = el.getBoundingClientRect().width / el.offsetWidth || 1
|
|
171
|
+
const o = src.getBoundingClientRect()
|
|
172
|
+
const t = text.getBoundingClientRect()
|
|
173
|
+
const dx = (o.left - t.left) / k
|
|
174
|
+
const dy = (o.top + o.height / 2 - (t.top + t.height / 2)) / k
|
|
175
|
+
const ease = 'cubic-bezier(0.22, 1, 0.36, 1)'
|
|
176
|
+
// Travels by left and top, so the words land without a sub-pixel hop
|
|
177
|
+
el.style.transition = 'none'
|
|
178
|
+
el.style.left = `${dx}px`
|
|
179
|
+
el.style.top = `${dy}px`
|
|
180
|
+
el.setAttribute('data-flying', '')
|
|
181
|
+
el.style.color = getComputedStyle(src).color
|
|
182
|
+
void el.offsetWidth
|
|
183
|
+
el.style.transition = `left 0.5s ${ease}, top 0.5s ${ease}, color 0.3s ease 0.06s`
|
|
184
|
+
el.style.left = el.style.top = ''
|
|
185
|
+
el.removeAttribute('data-flying')
|
|
186
|
+
el.style.color = ''
|
|
187
|
+
const end = (e: TransitionEvent) => {
|
|
188
|
+
if (e.target !== el || e.propertyName !== 'top') return
|
|
189
|
+
el.style.transition = ''
|
|
190
|
+
el.removeEventListener('transitionend', end)
|
|
191
|
+
}
|
|
192
|
+
el.addEventListener('transitionend', end)
|
|
193
|
+
return () => el.removeEventListener('transitionend', end)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const isBar = $derived(fidelity === 'minimal' && typeof children === 'string')
|
|
197
|
+
const barWidth = $derived(typeof children === 'string' ? Math.min(180, children.length * 5.5) : 0)
|
|
198
|
+
</script>
|
|
199
|
+
|
|
200
|
+
<div
|
|
201
|
+
class={`ss-bubble-row${className ? ` ${className}` : ''}${klass ? ` ${klass}` : ''}`}
|
|
202
|
+
data-from={from}
|
|
203
|
+
data-avatar={avatar ? '' : undefined}
|
|
204
|
+
style={style || undefined}
|
|
205
|
+
bind:this={rowEl}
|
|
206
|
+
>
|
|
207
|
+
{#if avatar && from !== 'me'}
|
|
208
|
+
<span class="ss-bubble__avatar" aria-hidden="true">{@render avatar()}</span>
|
|
209
|
+
{/if}
|
|
210
|
+
<div
|
|
211
|
+
{...rest}
|
|
212
|
+
bind:this={bubbleEl}
|
|
213
|
+
class="ss-bubble"
|
|
214
|
+
data-arrived={arrived || undefined}
|
|
215
|
+
data-source={source ? '' : undefined}
|
|
216
|
+
data-tail={effectiveTail || undefined}
|
|
217
|
+
data-typing={typing || undefined}
|
|
218
|
+
data-grew={grew || undefined}
|
|
219
|
+
data-tapback={tapback && tapback.show !== false ? '' : undefined}
|
|
220
|
+
>
|
|
221
|
+
{#if tapback}
|
|
222
|
+
<span class="ss-tapback" data-show={tapback.show !== false || undefined} aria-hidden="true">
|
|
223
|
+
<span class="ss-tapback__emoji">
|
|
224
|
+
{#if typeof tapback.emoji === 'string'}{tapback.emoji}{:else}{@render tapback.emoji()}{/if}
|
|
225
|
+
</span>
|
|
226
|
+
</span>
|
|
227
|
+
{/if}
|
|
228
|
+
{#if typing}
|
|
229
|
+
<span class="ss-bubble__dots" aria-label="typing">
|
|
230
|
+
<i></i>
|
|
231
|
+
<i></i>
|
|
232
|
+
<i></i>
|
|
233
|
+
</span>
|
|
234
|
+
{:else}
|
|
235
|
+
<span class="ss-bubble__text">
|
|
236
|
+
{#if isBar}
|
|
237
|
+
<span class="ss-bar ss-bubble__bar" style={`width:${barWidth}px`}></span>
|
|
238
|
+
{:else if typeof children === 'string'}
|
|
239
|
+
{children}
|
|
240
|
+
{:else if children}
|
|
241
|
+
{@render children()}
|
|
242
|
+
{/if}
|
|
243
|
+
</span>
|
|
244
|
+
{/if}
|
|
245
|
+
<!-- Always drawn, so a tail grows out of the corner when its bubble
|
|
246
|
+
becomes the last of a run, and retracts when it stops being. -->
|
|
247
|
+
<svg class="ss-bubble__tail" viewBox="0 0 18 16" aria-hidden="true">
|
|
248
|
+
<path
|
|
249
|
+
d={from === 'me'
|
|
250
|
+
? 'M0 0H12V8.5C12 12.6 14 15 17.5 16C15 16.7 12.2 16.5 9.8 15.3L0 15Z'
|
|
251
|
+
: 'M18 0H6V8.5C6 12.6 4 15 0.5 16C3 16.7 5.8 16.5 8.2 15.3L18 15Z'}
|
|
252
|
+
/>
|
|
253
|
+
</svg>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'class' | 'children'> {
|
|
4
|
+
/** me sits on the right in the accent bubble; them on the left. */
|
|
5
|
+
from: 'me' | 'them';
|
|
6
|
+
/** Three bouncing dots in place of the text. Turning it off grows the bubble into the message. */
|
|
7
|
+
typing?: boolean;
|
|
8
|
+
/** Set by Thread: true on the last bubble of a run. */
|
|
9
|
+
tail?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Where the message comes from, as iMessage sends: a target name (or a
|
|
12
|
+
* CSS selector) for the composer's text. The bubble forms out of it, the
|
|
13
|
+
* words lifting from the field into place while the bubble fills in around
|
|
14
|
+
* them. Clear the field in the same update that adds the bubble.
|
|
15
|
+
*/
|
|
16
|
+
source?: string;
|
|
17
|
+
/**
|
|
18
|
+
* A tapback on this bubble: the emoji, and `show` to pop it in and out.
|
|
19
|
+
* It sits in a white circle on the bubble's bottom corner, and the bubble
|
|
20
|
+
* makes room below so the emoji never covers the words.
|
|
21
|
+
*/
|
|
22
|
+
tapback?: {
|
|
23
|
+
emoji: Snippet | string;
|
|
24
|
+
show?: boolean;
|
|
25
|
+
};
|
|
26
|
+
/** A small `Avatar` beside a received bubble, as a group chat shows who is speaking. */
|
|
27
|
+
avatar?: Snippet;
|
|
28
|
+
className?: string;
|
|
29
|
+
/** Svelte's own class attribute; merged after className. */
|
|
30
|
+
class?: string;
|
|
31
|
+
style?: string;
|
|
32
|
+
/** Text, or a snippet for rich content. A string can also be passed to `children` directly. */
|
|
33
|
+
children?: Snippet | string;
|
|
34
|
+
}
|
|
35
|
+
declare const Bubble: import("svelte").Component<Props, {}, "">;
|
|
36
|
+
type Bubble = ReturnType<typeof Bubble>;
|
|
37
|
+
export default Bubble;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
/** While typing, a real terminal holds the cursor solid. */
|
|
4
|
+
still?: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let { still = false }: Props = $props()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<span class="ss-term__caret" data-still={still || undefined} aria-hidden="true"></span>
|