@vit-foundation/ui 0.29.0 → 0.31.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/README.md +51 -7
- package/dist/components/overlay/HoverCard.svelte +138 -0
- package/dist/components/overlay/HoverCard.svelte.d.ts +52 -0
- package/dist/components/overlay/anchor.d.ts +92 -0
- package/dist/components/overlay/anchor.js +61 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/overlay.d.ts +15 -0
- package/dist/overlay.js +15 -0
- package/dist/scrolly.d.ts +1 -0
- package/dist/styles/tokens.css +13 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -9,7 +9,10 @@ content editable in place.
|
|
|
9
9
|
npm install @vit-foundation/ui
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
Requires `svelte` ^5.0.0 as a peer.
|
|
12
|
+
Requires `svelte` ^5.0.0 as a peer. `ScrollySteps`, alone in the package,
|
|
13
|
+
also needs the optional peer `@sveltejs/svelte-scroller` — see
|
|
14
|
+
[scrolly](./docs/components/scrolly.md#installing-the-peer). Import the two
|
|
15
|
+
stylesheets once:
|
|
13
16
|
|
|
14
17
|
```svelte
|
|
15
18
|
<script>
|
|
@@ -29,6 +32,8 @@ Everything exports flat from the root, and again grouped by role:
|
|
|
29
32
|
| [`/chrome`](./docs/components/chrome.md) | PageShell, Nav, Footer |
|
|
30
33
|
| [`/content`](./docs/components/content.md) | WeeklieCard, ProjectCard, Timeline, TimelineMilestone, TeamMemberCard, CollaboratorList, JobList, SortSelect, the nine page modules (HomePage … WeeklyPage) — plus the data shapes, the page-copy vocabulary, helpers and the two list rules (createWeeklyList, createUrlFilters) |
|
|
31
34
|
| [`/community`](./docs/components/community.md) | AuthPageShell, LoginForm, SignupForm, GoogleAuthForm, AccountPanel, NewsletterSignup, CommentSection, ReactionBar, ContactForm |
|
|
35
|
+
| [`/admin`](./docs/components/admin.md) | DecorMosaic, PageHeading, Sidebar — shell-level composition for the foundation's internal tools |
|
|
36
|
+
| [`/scrolly`](./docs/components/scrolly.md) | ScrollySteps, ScrollyStepIndicator, CrossfadeVideo, GlassCard, the stepStyle ramp — the only entry point that does NOT re-export from the root, and the only one with a peer of its own |
|
|
32
37
|
| [`/edit`](./docs/edit-mode.md) | Editable, setEditAdapter(adapter, EDIT_CHROME)/getEditAdapter, descriptors and helpers, collectionEditing, LocalizedText |
|
|
33
38
|
| [`/config`](./docs/getting-started.md#wiring-an-app-uiprovider) | UiProvider, UiConfig, the locale set, the default Catalan messages |
|
|
34
39
|
| `/contract` | The component-free half: LOCALES, BASE_LOCALE, localize, REACTIONS, PAGE_COPY_KEYS and the edit-descriptor types — the one subpath a host may import from SERVER code |
|
|
@@ -50,7 +55,11 @@ integrates its i18n and router through one `UiProvider` in the root layout.
|
|
|
50
55
|
- **Component reference** — [primitives](./docs/components/primitives.md) ·
|
|
51
56
|
[chrome](./docs/components/chrome.md) ·
|
|
52
57
|
[content](./docs/components/content.md) ·
|
|
53
|
-
[community](./docs/components/community.md)
|
|
58
|
+
[community](./docs/components/community.md) ·
|
|
59
|
+
[admin](./docs/components/admin.md) ·
|
|
60
|
+
[scrolly](./docs/components/scrolly.md)
|
|
61
|
+
- **[Changelog](./docs/changelog/index.md)** — one page per version, newest
|
|
62
|
+
first; [unreleased](./docs/changelog/unreleased.md) is what is on `main`
|
|
54
63
|
- **Storybook** — `npm run storybook`: every component has a story;
|
|
55
64
|
`Edit mode/EditMode` demos the whole editing loop against an in-memory
|
|
56
65
|
adapter
|
|
@@ -84,8 +93,43 @@ diffable against history. The public structure is the semantic entry points
|
|
|
84
93
|
above, assembled in `src/lib/{primitives,chrome,content-components,community}.ts`
|
|
85
94
|
and `src/lib/forms/index.ts`.
|
|
86
95
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
## Cutting a release
|
|
97
|
+
|
|
98
|
+
Releases are MANUAL. The workflow this section used to describe fired on every
|
|
99
|
+
`v*` tag, had no `NPM_TOKEN` and no lint, check or test step, so it failed
|
|
100
|
+
every time and was deleted; `prepublishOnly` (lint, check, unit tests) is the
|
|
101
|
+
only publish gate now.
|
|
102
|
+
|
|
103
|
+
**While you work**, add the entry to
|
|
104
|
+
[`docs/changelog/unreleased.md`](./docs/changelog/unreleased.md) in the same PR
|
|
105
|
+
as the change, under one of Keep a Changelog's types — `Added`, `Changed`,
|
|
106
|
+
`Deprecated`, `Removed`, `Fixed`, `Security` — or
|
|
107
|
+
`Other (dependencies, CI, tools…)` for what a consumer never sees. One fact,
|
|
108
|
+
one entry: if a type already covers the thing you changed, edit that entry so
|
|
109
|
+
it describes the end state. A change a consumer must act on before upgrading
|
|
110
|
+
carries an **Upgrading:** paragraph.
|
|
111
|
+
|
|
112
|
+
**On release day**, in one commit (`chore(release): x.y.z`):
|
|
113
|
+
|
|
114
|
+
1. Read `unreleased.md` once as a whole and merge what the individual PRs
|
|
115
|
+
restated.
|
|
116
|
+
2. `git mv docs/changelog/unreleased.md docs/changelog/x.y.z.md`. Retitle it
|
|
117
|
+
`# x.y.z`, date it, and point its compare link at the tag range rather than
|
|
118
|
+
at `main`.
|
|
119
|
+
3. Write a fresh `unreleased.md` with the same header and no entries — copy the
|
|
120
|
+
one you just renamed rather than inventing a new shape.
|
|
121
|
+
4. Add the row to [`docs/changelog/index.md`](./docs/changelog/index.md).
|
|
122
|
+
5. `npm version x.y.z` — MINOR for new exports and features, PATCH for fixes
|
|
123
|
+
alone, MAJOR for anything that breaks a consumer's imports or markup.
|
|
124
|
+
6. `npm publish`. Check `npm view @vit-foundation/ui version` first: the
|
|
125
|
+
registry has been ahead of `main` before.
|
|
126
|
+
7. `git push && git push origin vx.y.z`. Nothing automated reads the tag, but
|
|
127
|
+
the compare links in the changelog do.
|
|
128
|
+
|
|
129
|
+
**Publish from a clean checkout of `main`, never from a tree with uncommitted
|
|
130
|
+
work.** 0.28.0 was published from one: it carried an icon that reached npm and
|
|
131
|
+
nothing else, and the next release would have silently removed it again if the
|
|
132
|
+
tarball had not been diffed against the source first.
|
|
133
|
+
|
|
134
|
+
A released page is history: never rewrite one to match later code. If it is
|
|
135
|
+
wrong, say so on the next version's page.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component HoverCard
|
|
3
|
+
|
|
4
|
+
The chrome of a hover card: a fixed-width surface with a shimmer skeleton
|
|
5
|
+
while its content is still resolving.
|
|
6
|
+
|
|
7
|
+
It owns the *shell*, never the content — what goes inside is the host's
|
|
8
|
+
snippet. It pairs with {@link ./anchor}, which decides where it goes, and the
|
|
9
|
+
two are separate on purpose: placement is arithmetic a test can check, chrome
|
|
10
|
+
is CSS a test cannot.
|
|
11
|
+
|
|
12
|
+
## Why the shimmer is here
|
|
13
|
+
A hover card whose content arrives asynchronously flickers between empty and
|
|
14
|
+
full. Showing a skeleton for a beat makes the transition deliberate, and doing
|
|
15
|
+
it on *every* new target rather than only on slow ones keeps the rhythm
|
|
16
|
+
even — a card that sometimes shimmers and sometimes does not reads as jank.
|
|
17
|
+
Remount the component (a `{#key}` on the hovered target) and it shimmers
|
|
18
|
+
again; pass `skipShimmer` for a clone that is animating out, which should fade
|
|
19
|
+
real content rather than a fresh skeleton.
|
|
20
|
+
|
|
21
|
+
## Theming
|
|
22
|
+
Presentation is CSS custom properties with plain fallbacks, so the card
|
|
23
|
+
renders standalone and restyles without a CSS framework:
|
|
24
|
+
|
|
25
|
+
| property | default |
|
|
26
|
+
|----------------------------|-----------|
|
|
27
|
+
| `--vit-card-font` | `inherit` |
|
|
28
|
+
| `--vit-card-width` | `277px` |
|
|
29
|
+
| `--vit-card-padding` | `10px` |
|
|
30
|
+
| `--vit-card-gap` | `10px` |
|
|
31
|
+
| `--vit-card-bg` | `#ffffff` |
|
|
32
|
+
| `--vit-card-radius` | `0` |
|
|
33
|
+
| `--vit-card-shadow` | `none` |
|
|
34
|
+
| `--vit-card-shimmer-color` | `currentColor` |
|
|
35
|
+
|
|
36
|
+
@property skipShimmer - Suppresses the mount shimmer, for a card fading out
|
|
37
|
+
@property shimmerMs - How long the skeleton is held, in ms
|
|
38
|
+
@property lines - Skeleton rows drawn under the heading block
|
|
39
|
+
@property class - Extra classes appended to the card
|
|
40
|
+
@property children - The card's content
|
|
41
|
+
-->
|
|
42
|
+
<script lang="ts">
|
|
43
|
+
import { onMount, type Snippet } from 'svelte';
|
|
44
|
+
|
|
45
|
+
let {
|
|
46
|
+
skipShimmer = false,
|
|
47
|
+
shimmerMs = 150,
|
|
48
|
+
lines = 3,
|
|
49
|
+
class: className = '',
|
|
50
|
+
children
|
|
51
|
+
}: {
|
|
52
|
+
skipShimmer?: boolean;
|
|
53
|
+
shimmerMs?: number;
|
|
54
|
+
lines?: number;
|
|
55
|
+
class?: string;
|
|
56
|
+
children: Snippet;
|
|
57
|
+
} = $props();
|
|
58
|
+
|
|
59
|
+
// Fixed per mount — the leaving clone must never re-shimmer.
|
|
60
|
+
// svelte-ignore state_referenced_locally
|
|
61
|
+
let shimmer = $state(!skipShimmer);
|
|
62
|
+
|
|
63
|
+
onMount(() => {
|
|
64
|
+
if (!shimmer) return;
|
|
65
|
+
const t = setTimeout(() => (shimmer = false), shimmerMs);
|
|
66
|
+
return () => clearTimeout(t);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** Descending widths, so the skeleton reads as text rather than as bars. */
|
|
70
|
+
const widths = $derived(Array.from({ length: Math.max(0, lines) }, (_, i) => `${100 - i * 25}%`));
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<div class="vit-card {className}">
|
|
74
|
+
{#if shimmer}
|
|
75
|
+
<div class="vit-card__skeleton">
|
|
76
|
+
<span class="vit-card__line" style="height: 12px; width: 100%"></span>
|
|
77
|
+
<span class="vit-card__line" style="height: 8px; width: 5rem"></span>
|
|
78
|
+
<span class="vit-card__rows">
|
|
79
|
+
{#each widths as w, i (i)}
|
|
80
|
+
<span class="vit-card__line" style="height: 17px; width: {w}"></span>
|
|
81
|
+
{/each}
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
84
|
+
{:else}
|
|
85
|
+
{@render children()}
|
|
86
|
+
{/if}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<style>
|
|
90
|
+
.vit-card {
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-direction: column;
|
|
93
|
+
gap: var(--vit-card-gap, 10px);
|
|
94
|
+
width: var(--vit-card-width, 277px);
|
|
95
|
+
padding: var(--vit-card-padding, 10px);
|
|
96
|
+
overflow: hidden;
|
|
97
|
+
background: var(--vit-card-bg, #ffffff);
|
|
98
|
+
border-radius: var(--vit-card-radius, 0);
|
|
99
|
+
box-shadow: var(--vit-card-shadow, none);
|
|
100
|
+
font-family: var(--vit-card-font, inherit);
|
|
101
|
+
}
|
|
102
|
+
.vit-card__skeleton {
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
gap: 6px;
|
|
106
|
+
}
|
|
107
|
+
.vit-card__rows {
|
|
108
|
+
display: flex;
|
|
109
|
+
flex-direction: column;
|
|
110
|
+
gap: 4px;
|
|
111
|
+
margin-top: 8px;
|
|
112
|
+
}
|
|
113
|
+
.vit-card__line {
|
|
114
|
+
display: block;
|
|
115
|
+
border-radius: 2px;
|
|
116
|
+
background: var(--vit-card-shimmer-color, currentColor);
|
|
117
|
+
animation: vit-card-shimmer 400ms ease-in-out infinite;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@keyframes vit-card-shimmer {
|
|
121
|
+
0% {
|
|
122
|
+
opacity: 0.3;
|
|
123
|
+
}
|
|
124
|
+
50% {
|
|
125
|
+
opacity: 0.6;
|
|
126
|
+
}
|
|
127
|
+
100% {
|
|
128
|
+
opacity: 0.3;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@media (prefers-reduced-motion: reduce) {
|
|
133
|
+
.vit-card__line {
|
|
134
|
+
animation: none;
|
|
135
|
+
opacity: 0.3;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
skipShimmer?: boolean;
|
|
4
|
+
shimmerMs?: number;
|
|
5
|
+
lines?: number;
|
|
6
|
+
class?: string;
|
|
7
|
+
children: Snippet;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* HoverCard
|
|
11
|
+
*
|
|
12
|
+
* The chrome of a hover card: a fixed-width surface with a shimmer skeleton
|
|
13
|
+
* while its content is still resolving.
|
|
14
|
+
*
|
|
15
|
+
* It owns the *shell*, never the content — what goes inside is the host's
|
|
16
|
+
* snippet. It pairs with {@link ./anchor}, which decides where it goes, and the
|
|
17
|
+
* two are separate on purpose: placement is arithmetic a test can check, chrome
|
|
18
|
+
* is CSS a test cannot.
|
|
19
|
+
*
|
|
20
|
+
* ## Why the shimmer is here
|
|
21
|
+
* A hover card whose content arrives asynchronously flickers between empty and
|
|
22
|
+
* full. Showing a skeleton for a beat makes the transition deliberate, and doing
|
|
23
|
+
* it on *every* new target rather than only on slow ones keeps the rhythm
|
|
24
|
+
* even — a card that sometimes shimmers and sometimes does not reads as jank.
|
|
25
|
+
* Remount the component (a `{#key}` on the hovered target) and it shimmers
|
|
26
|
+
* again; pass `skipShimmer` for a clone that is animating out, which should fade
|
|
27
|
+
* real content rather than a fresh skeleton.
|
|
28
|
+
*
|
|
29
|
+
* ## Theming
|
|
30
|
+
* Presentation is CSS custom properties with plain fallbacks, so the card
|
|
31
|
+
* renders standalone and restyles without a CSS framework:
|
|
32
|
+
*
|
|
33
|
+
* | property | default |
|
|
34
|
+
* |----------------------------|-----------|
|
|
35
|
+
* | `--vit-card-font` | `inherit` |
|
|
36
|
+
* | `--vit-card-width` | `277px` |
|
|
37
|
+
* | `--vit-card-padding` | `10px` |
|
|
38
|
+
* | `--vit-card-gap` | `10px` |
|
|
39
|
+
* | `--vit-card-bg` | `#ffffff` |
|
|
40
|
+
* | `--vit-card-radius` | `0` |
|
|
41
|
+
* | `--vit-card-shadow` | `none` |
|
|
42
|
+
* | `--vit-card-shimmer-color` | `currentColor` |
|
|
43
|
+
*
|
|
44
|
+
* @property skipShimmer - Suppresses the mount shimmer, for a card fading out
|
|
45
|
+
* @property shimmerMs - How long the skeleton is held, in ms
|
|
46
|
+
* @property lines - Skeleton rows drawn under the heading block
|
|
47
|
+
* @property class - Extra classes appended to the card
|
|
48
|
+
* @property children - The card's content
|
|
49
|
+
*/
|
|
50
|
+
declare const HoverCard: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
51
|
+
type HoverCard = ReturnType<typeof HoverCard>;
|
|
52
|
+
export default HoverCard;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module components/overlay/anchor
|
|
3
|
+
* Where a floating card goes when it is attached to a point.
|
|
4
|
+
*
|
|
5
|
+
* "Put a card next to the pointer without letting it leave the container" is a
|
|
6
|
+
* rule every hover tooltip needs and nobody writes down. It gets re-derived at
|
|
7
|
+
* each call site, slightly differently each time, and because it only runs
|
|
8
|
+
* inside a live layout it is never tested — so the version that forgot to clamp
|
|
9
|
+
* vertically ships a card that falls off the bottom of a short viewport, and
|
|
10
|
+
* nobody notices until a phone does it.
|
|
11
|
+
*
|
|
12
|
+
* It is one function here, it takes measurements rather than elements, and it
|
|
13
|
+
* returns two numbers. No DOM, no framework, no CSS: the caller owns all three.
|
|
14
|
+
*/
|
|
15
|
+
/** The point the card is attached to, in container coordinates. */
|
|
16
|
+
export type AnchorPoint = {
|
|
17
|
+
/** Distance from the container's left edge, in px. */
|
|
18
|
+
x: number;
|
|
19
|
+
/** Distance from the container's top edge, in px. */
|
|
20
|
+
y: number;
|
|
21
|
+
};
|
|
22
|
+
/** A measured rectangle, in px. */
|
|
23
|
+
export type AnchorBox = {
|
|
24
|
+
/** Width in px. */
|
|
25
|
+
width: number;
|
|
26
|
+
/** Height in px. */
|
|
27
|
+
height: number;
|
|
28
|
+
};
|
|
29
|
+
/** Space to keep between the card and each container edge, in px. */
|
|
30
|
+
export type AnchorPadding = {
|
|
31
|
+
/** Minimum gap above the card. */
|
|
32
|
+
top?: number;
|
|
33
|
+
/** Minimum gap to the right of the card. */
|
|
34
|
+
right?: number;
|
|
35
|
+
/** Minimum gap below the card. */
|
|
36
|
+
bottom?: number;
|
|
37
|
+
/** Minimum gap to the left of the card. */
|
|
38
|
+
left?: number;
|
|
39
|
+
};
|
|
40
|
+
/** How the card is placed relative to the point, before clamping. */
|
|
41
|
+
export type AnchorOptions = {
|
|
42
|
+
/**
|
|
43
|
+
* Gap between the point and the card's near edge, in px. `x` is the
|
|
44
|
+
* horizontal offset to whichever side the card lands on; `y` shifts the
|
|
45
|
+
* card vertically and is only read when `align` is `'start'`.
|
|
46
|
+
*/
|
|
47
|
+
offset?: {
|
|
48
|
+
x?: number;
|
|
49
|
+
y?: number;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Vertical relationship to the point: `'center'` centres the card on it,
|
|
53
|
+
* `'start'` puts the card's top edge there (plus `offset.y`).
|
|
54
|
+
*/
|
|
55
|
+
align?: 'center' | 'start';
|
|
56
|
+
/** Minimum gaps from the container edges. */
|
|
57
|
+
padding?: AnchorPadding;
|
|
58
|
+
/**
|
|
59
|
+
* When the card does not fit on its preferred side, put it on the other one
|
|
60
|
+
* instead of letting the clamp slide it back over the point. Leave it off
|
|
61
|
+
* for a card that should simply stay put.
|
|
62
|
+
*/
|
|
63
|
+
flip?: boolean;
|
|
64
|
+
/** Preferred horizontal side. Default `'right'`. */
|
|
65
|
+
side?: 'right' | 'left';
|
|
66
|
+
};
|
|
67
|
+
/** The resolved position, ready to write to `left` / `top` in px. */
|
|
68
|
+
export type AnchorPosition = {
|
|
69
|
+
/** Distance from the container's left edge, in px. */
|
|
70
|
+
left: number;
|
|
71
|
+
/** Distance from the container's top edge, in px. */
|
|
72
|
+
top: number;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Places a card beside a point, kept inside its container.
|
|
76
|
+
*
|
|
77
|
+
* The card is laid out on its preferred side, optionally flipped to the other
|
|
78
|
+
* side when it would not fit, and then clamped on **both** axes so it can never
|
|
79
|
+
* cross the container's padding — including vertically, which is the half most
|
|
80
|
+
* hand-rolled versions leave out.
|
|
81
|
+
*
|
|
82
|
+
* A container with no measured size yet (width or height `0`) returns the raw
|
|
83
|
+
* point, so a first frame renders somewhere sensible rather than at `0,0`.
|
|
84
|
+
*
|
|
85
|
+
* @param point - Where the card is attached, in container coordinates.
|
|
86
|
+
* @param card - The card's measured size. Height may be an estimate on the
|
|
87
|
+
* first frame; re-running once it is measured is cheap and exact.
|
|
88
|
+
* @param container - The box the card must stay inside.
|
|
89
|
+
* @param options - Offset, alignment, padding, flipping and preferred side.
|
|
90
|
+
* @returns The `left` / `top` to position the card at, in px.
|
|
91
|
+
*/
|
|
92
|
+
export declare function anchor(point: AnchorPoint, card: AnchorBox, container: AnchorBox, options?: AnchorOptions): AnchorPosition;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module components/overlay/anchor
|
|
3
|
+
* Where a floating card goes when it is attached to a point.
|
|
4
|
+
*
|
|
5
|
+
* "Put a card next to the pointer without letting it leave the container" is a
|
|
6
|
+
* rule every hover tooltip needs and nobody writes down. It gets re-derived at
|
|
7
|
+
* each call site, slightly differently each time, and because it only runs
|
|
8
|
+
* inside a live layout it is never tested — so the version that forgot to clamp
|
|
9
|
+
* vertically ships a card that falls off the bottom of a short viewport, and
|
|
10
|
+
* nobody notices until a phone does it.
|
|
11
|
+
*
|
|
12
|
+
* It is one function here, it takes measurements rather than elements, and it
|
|
13
|
+
* returns two numbers. No DOM, no framework, no CSS: the caller owns all three.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Places a card beside a point, kept inside its container.
|
|
17
|
+
*
|
|
18
|
+
* The card is laid out on its preferred side, optionally flipped to the other
|
|
19
|
+
* side when it would not fit, and then clamped on **both** axes so it can never
|
|
20
|
+
* cross the container's padding — including vertically, which is the half most
|
|
21
|
+
* hand-rolled versions leave out.
|
|
22
|
+
*
|
|
23
|
+
* A container with no measured size yet (width or height `0`) returns the raw
|
|
24
|
+
* point, so a first frame renders somewhere sensible rather than at `0,0`.
|
|
25
|
+
*
|
|
26
|
+
* @param point - Where the card is attached, in container coordinates.
|
|
27
|
+
* @param card - The card's measured size. Height may be an estimate on the
|
|
28
|
+
* first frame; re-running once it is measured is cheap and exact.
|
|
29
|
+
* @param container - The box the card must stay inside.
|
|
30
|
+
* @param options - Offset, alignment, padding, flipping and preferred side.
|
|
31
|
+
* @returns The `left` / `top` to position the card at, in px.
|
|
32
|
+
*/
|
|
33
|
+
export function anchor(point, card, container, options = {}) {
|
|
34
|
+
if (!container.width || !container.height)
|
|
35
|
+
return { left: point.x, top: point.y };
|
|
36
|
+
const offsetX = options.offset?.x ?? 16;
|
|
37
|
+
const offsetY = options.offset?.y ?? 0;
|
|
38
|
+
const padTop = options.padding?.top ?? 0;
|
|
39
|
+
const padRight = options.padding?.right ?? 0;
|
|
40
|
+
const padBottom = options.padding?.bottom ?? 0;
|
|
41
|
+
const padLeft = options.padding?.left ?? 0;
|
|
42
|
+
const toLeftOf = point.x - offsetX - card.width;
|
|
43
|
+
const toRightOf = point.x + offsetX;
|
|
44
|
+
let left = options.side === 'left' ? toLeftOf : toRightOf;
|
|
45
|
+
if (options.flip) {
|
|
46
|
+
if (options.side === 'left') {
|
|
47
|
+
if (left < padLeft && toRightOf + card.width <= container.width - padRight)
|
|
48
|
+
left = toRightOf;
|
|
49
|
+
}
|
|
50
|
+
else if (left + card.width > container.width - padRight && toLeftOf >= padLeft) {
|
|
51
|
+
left = toLeftOf;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
let top = options.align === 'start' ? point.y + offsetY : point.y - card.height / 2;
|
|
55
|
+
// Clamp last, on both axes. `Math.max` after `Math.min` so a card taller or
|
|
56
|
+
// wider than the space it has left is pinned to the near edge rather than
|
|
57
|
+
// pushed off the far one.
|
|
58
|
+
left = Math.max(padLeft, Math.min(left, container.width - card.width - padRight));
|
|
59
|
+
top = Math.max(padTop, Math.min(top, container.height - card.height - padBottom));
|
|
60
|
+
return { left, top };
|
|
61
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -16,3 +16,4 @@ export * from './edit/index.js';
|
|
|
16
16
|
// server-side rules, so they go through ./contract instead — see the note in
|
|
17
17
|
// utils/paths.ts, which is where the export surface for all of them is decided.
|
|
18
18
|
export { buildQueryString } from './utils/paths.js';
|
|
19
|
+
export { anchor, HoverCard } from './overlay.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floating-overlay primitives: where a card attached to a point goes, and the
|
|
3
|
+
* chrome it is drawn in.
|
|
4
|
+
*
|
|
5
|
+
* The two are separate because they fail differently. Placement is arithmetic —
|
|
6
|
+
* it can be wrong on a phone and right on a laptop, and it belongs in a test.
|
|
7
|
+
* Chrome is CSS, and belongs behind custom properties. Both were previously
|
|
8
|
+
* re-implemented per call site, which is how one copy ended up clamping
|
|
9
|
+
* horizontally but not vertically.
|
|
10
|
+
*
|
|
11
|
+
* Domain-free: it knows about points, boxes and padding, and nothing about maps,
|
|
12
|
+
* charts or what the card says.
|
|
13
|
+
*/
|
|
14
|
+
export { anchor, type AnchorBox, type AnchorOptions, type AnchorPadding, type AnchorPoint, type AnchorPosition } from './components/overlay/anchor.js';
|
|
15
|
+
export { default as HoverCard } from './components/overlay/HoverCard.svelte';
|
package/dist/overlay.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floating-overlay primitives: where a card attached to a point goes, and the
|
|
3
|
+
* chrome it is drawn in.
|
|
4
|
+
*
|
|
5
|
+
* The two are separate because they fail differently. Placement is arithmetic —
|
|
6
|
+
* it can be wrong on a phone and right on a laptop, and it belongs in a test.
|
|
7
|
+
* Chrome is CSS, and belongs behind custom properties. Both were previously
|
|
8
|
+
* re-implemented per call site, which is how one copy ended up clamping
|
|
9
|
+
* horizontally but not vertically.
|
|
10
|
+
*
|
|
11
|
+
* Domain-free: it knows about points, boxes and padding, and nothing about maps,
|
|
12
|
+
* charts or what the card says.
|
|
13
|
+
*/
|
|
14
|
+
export { anchor } from './components/overlay/anchor.js';
|
|
15
|
+
export { default as HoverCard } from './components/overlay/HoverCard.svelte';
|
package/dist/scrolly.d.ts
CHANGED
|
@@ -10,5 +10,6 @@
|
|
|
10
10
|
export { default as ScrollySteps } from './components/scrolly/ScrollySteps.svelte';
|
|
11
11
|
export { default as ScrollyStepIndicator } from './components/scrolly/ScrollyStepIndicator.svelte';
|
|
12
12
|
export { default as CrossfadeVideo } from './components/scrolly/CrossfadeVideo.svelte';
|
|
13
|
+
export type { CrossfadeVideoHandle } from './components/scrolly/CrossfadeVideo.svelte';
|
|
13
14
|
export { default as GlassCard } from './components/scrolly/GlassCard.svelte';
|
|
14
15
|
export { calcOpacity, calcTranslateY, stepStyle, type StepStyle } from './components/scrolly/stepStyle.js';
|
package/dist/styles/tokens.css
CHANGED
|
@@ -79,4 +79,17 @@
|
|
|
79
79
|
--video-badge-padding: var(--space-3);
|
|
80
80
|
--video-badge-color: var(--color-ink);
|
|
81
81
|
--video-badge-bg: rgb(255 255 255 / 50%);
|
|
82
|
+
|
|
83
|
+
/* Floating overlays (the `./overlay` subpath) */
|
|
84
|
+
/* The hover card's fixed width. Fixed rather than fluid because the card is
|
|
85
|
+
positioned by measuring it: a width that depends on its content makes the
|
|
86
|
+
placement arithmetic chase itself for a frame. */
|
|
87
|
+
--vit-card-width: 277px;
|
|
88
|
+
--vit-card-padding: var(--space-2, 10px);
|
|
89
|
+
--vit-card-gap: var(--space-2, 10px);
|
|
90
|
+
--vit-card-bg: #ffffff;
|
|
91
|
+
--vit-card-radius: 0;
|
|
92
|
+
--vit-card-shadow: none;
|
|
93
|
+
--vit-card-font: inherit;
|
|
94
|
+
--vit-card-shimmer-color: currentColor;
|
|
82
95
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vit-foundation/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "vite build && npm run prepack",
|
|
@@ -96,6 +96,11 @@
|
|
|
96
96
|
"types": "./dist/scrolly.d.ts",
|
|
97
97
|
"svelte": "./dist/scrolly.js",
|
|
98
98
|
"default": "./dist/scrolly.js"
|
|
99
|
+
},
|
|
100
|
+
"./overlay": {
|
|
101
|
+
"types": "./dist/overlay.d.ts",
|
|
102
|
+
"svelte": "./dist/overlay.js",
|
|
103
|
+
"default": "./dist/overlay.js"
|
|
99
104
|
}
|
|
100
105
|
},
|
|
101
106
|
"peerDependencies": {
|