@cratis/cast 0.0.1 → 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/README.md +13 -1
- package/badges/RegistryMember.ts +18 -0
- package/badges/ResolvedTier.ts +17 -0
- package/badges/Tier.ts +14 -0
- package/badges/TierSource.ts +14 -0
- package/badges/badgeSvg.ts +137 -0
- package/badges/constants.ts +17 -0
- package/badges/cratisMark.ts +15 -0
- package/badges/index.ts +12 -0
- package/badges/registry.ts +68 -0
- package/badges/resolveTier.ts +24 -0
- package/cast-json.schema.json +1496 -0
- package/cast.schema.json +1113 -0
- package/config/CastConfig.ts +17 -0
- package/config/CastConfigError.ts +10 -0
- package/config/LiveStat.ts +17 -0
- package/config/ResolvedSection.ts +13 -0
- package/config/SectionId.ts +21 -0
- package/config/ThemeMode.ts +9 -0
- package/config/calendar.ts +34 -0
- package/config/castSchema.ts +263 -0
- package/config/index.ts +12 -0
- package/config/parseCast.ts +58 -0
- package/config/resolveSections.ts +48 -0
- package/github/GitHubSnapshot.ts +66 -0
- package/github/fetchGitHub.ts +235 -0
- package/github/gitHubSnapshotSchema.ts +58 -0
- package/github/parse.ts +63 -0
- package/index.ts +88 -0
- package/integration/CastData.ts +20 -0
- package/integration/CastOptions.ts +17 -0
- package/integration/Snapshot.ts +12 -0
- package/integration/loadRegistry.ts +37 -0
- package/integration/resolveData.ts +138 -0
- package/integration/snapshotFile.ts +23 -0
- package/integration/virtualModule.ts +43 -0
- package/machine/castJson.ts +77 -0
- package/machine/castJsonSchema.ts +35 -0
- package/machine/index.ts +6 -0
- package/machine/llmsText.ts +69 -0
- package/machine/ogImageSvg.ts +79 -0
- package/machine/profileFromCastJson.ts +214 -0
- package/machine/resumeJson.ts +87 -0
- package/package.json +80 -8
- package/registry/members.yaml +16 -0
- package/routes/404.astro +42 -0
- package/routes/apple-touch-icon.png.ts +17 -0
- package/routes/badge-card.svg.ts +13 -0
- package/routes/badge.svg.ts +11 -0
- package/routes/cast.json.ts +17 -0
- package/routes/cv.astro +9 -0
- package/routes/favicon.svg.ts +16 -0
- package/routes/index.astro +9 -0
- package/routes/llms.txt.ts +10 -0
- package/routes/og.png.ts +23 -0
- package/routes/resume.json.ts +13 -0
- package/routes/robots.txt.ts +10 -0
- package/site/components/Activity.astro +196 -0
- package/site/components/Agentic.astro +144 -0
- package/site/components/CareerReplay.astro +415 -0
- package/site/components/CastBadge.astro +132 -0
- package/site/components/CommandPalette.astro +122 -0
- package/site/components/Contact.astro +166 -0
- package/site/components/Cratis.astro +235 -0
- package/site/components/CratisMark.astro +18 -0
- package/site/components/EventModel.astro +265 -0
- package/site/components/Footer.astro +33 -0
- package/site/components/Hero.astro +211 -0
- package/site/components/Inline.astro +45 -0
- package/site/components/Monogram.astro +28 -0
- package/site/components/Nav.astro +127 -0
- package/site/components/Now.astro +116 -0
- package/site/components/Projects.astro +185 -0
- package/site/components/PullRequestFeed.astro +88 -0
- package/site/components/SectionHeader.astro +30 -0
- package/site/components/Skills.astro +44 -0
- package/site/components/Stats.astro +106 -0
- package/site/components/Talks.astro +76 -0
- package/site/components/Terminal.astro +164 -0
- package/site/components/Writing.astro +128 -0
- package/site/data.ts +11 -0
- package/site/layouts/Base.astro +156 -0
- package/site/lib/accent.ts +52 -0
- package/site/lib/blog.ts +58 -0
- package/site/lib/career.ts +148 -0
- package/site/lib/clipboard.ts +12 -0
- package/site/lib/contributions.ts +63 -0
- package/site/lib/github.ts +72 -0
- package/site/lib/inline.ts +79 -0
- package/site/lib/lifecycle.ts +44 -0
- package/site/lib/motion.ts +43 -0
- package/site/lib/nav.ts +49 -0
- package/site/lib/reltime.ts +33 -0
- package/site/lib/safeJson.ts +15 -0
- package/site/lib/sections.ts +41 -0
- package/site/lib/theme.ts +28 -0
- package/site/pages/CvPage.astro +379 -0
- package/site/pages/ProfilePage.astro +78 -0
- package/site/profile/CastHost.ts +32 -0
- package/site/profile/CastView.ts +96 -0
- package/site/profile/assertRenderable.ts +151 -0
- package/site/profile/clientCast.ts +117 -0
- package/site/profile/index.ts +8 -0
- package/site/scripts/ambient.ts +93 -0
- package/site/scripts/event-board.ts +403 -0
- package/site/scripts/event-model.ts +155 -0
- package/site/scripts/heatmap.ts +94 -0
- package/site/scripts/live.ts +155 -0
- package/site/scripts/palette.ts +313 -0
- package/site/scripts/replay.ts +252 -0
- package/site/scripts/terminal.ts +559 -0
- package/site/styles/global.css +508 -0
- package/virtual.d.ts +15 -0
- package/writing/WritingSnapshot.ts +20 -0
- package/writing/fetchWriting.ts +69 -0
- package/writing/parse.ts +72 -0
- package/writing/writingSnapshotSchema.ts +27 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { isCalendarDate, isCalendarMonth, isInstant, isPartialDate } from '../../config/calendar';
|
|
5
|
+
import { accentTokens } from '../lib/accent';
|
|
6
|
+
import { alsoText, apply, formatMonth, initialProjection, normalizeDate, yearsBetween } from '../lib/career';
|
|
7
|
+
import { foldCalendar, formatDay, formatMonthLong } from '../lib/contributions';
|
|
8
|
+
import { parseInline, plainText } from '../lib/inline';
|
|
9
|
+
import { safeJson } from '../lib/safeJson';
|
|
10
|
+
import { clientCast } from './clientCast';
|
|
11
|
+
import type { CastView } from './CastView';
|
|
12
|
+
|
|
13
|
+
const webProtocols = new Set(['http:', 'https:']);
|
|
14
|
+
|
|
15
|
+
function fail(what: string, value: unknown): never {
|
|
16
|
+
throw new Error(`${what}: cannot render ${JSON.stringify(value)?.slice(0, 120)}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function url(what: string, value: string, base?: string, protocols: ReadonlySet<string> = webProtocols): void {
|
|
20
|
+
let parsed: URL;
|
|
21
|
+
try {
|
|
22
|
+
parsed = new URL(value, base);
|
|
23
|
+
} catch {
|
|
24
|
+
fail(what, value);
|
|
25
|
+
}
|
|
26
|
+
if (!protocols.has(parsed.protocol)) fail(what, value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function count(what: string, value: number | undefined): void {
|
|
30
|
+
if (value !== undefined && !(Number.isSafeInteger(value) && value >= 0)) fail(what, value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A date the components format: an unreadable one shows as 'Invalid Date', or as a month name that does not exist. */
|
|
34
|
+
function shown(what: string, formatted: string, value: string): void {
|
|
35
|
+
if (formatted.includes('Invalid Date') || formatted.startsWith(' ')) fail(what, value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function inline(what: string, text: string | undefined): void {
|
|
39
|
+
if (text === undefined) return;
|
|
40
|
+
if (typeof text !== 'string') fail(what, text);
|
|
41
|
+
parseInline(text);
|
|
42
|
+
plainText(text);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function links(cast: CastView): void {
|
|
46
|
+
const { config, person, github, posts } = cast;
|
|
47
|
+
for (const [key, value] of Object.entries(person.links)) url(`person.links.${key}`, value);
|
|
48
|
+
if (person.email) url('person.email', `mailto:${person.email}`, undefined, new Set(['mailto:']));
|
|
49
|
+
url('site.url', config.site.url);
|
|
50
|
+
if (config.site.source) url('site.source', config.site.source);
|
|
51
|
+
// Base resolves the portrait against the site; the directory resolves relative links in text against the member's site.
|
|
52
|
+
if (cast.photoUrl) url('photo', cast.photoUrl, config.site.url, new Set(['https:']));
|
|
53
|
+
if (cast.host.linkOrigin) url('linkOrigin', cast.host.linkOrigin);
|
|
54
|
+
for (const [key, value] of Object.entries(cast.cratis?.links ?? {})) url(`cratis.links.${key}`, value);
|
|
55
|
+
for (const repo of cast.cratisRepos) if (repo.url) url(`cratis.repos.${repo.name}`, repo.url);
|
|
56
|
+
for (const project of cast.liveProjects) url(`projects.${project.name}`, project.url);
|
|
57
|
+
for (const talk of cast.talks) if (talk.url) url(`talks.${talk.title}`, talk.url);
|
|
58
|
+
for (const certification of cast.certifications) if (certification.url) url(`certifications.${certification.title}`, certification.url);
|
|
59
|
+
for (const item of cast.agentic?.items ?? []) if (item.url) url(`agentic.items.${item.name}`, item.url);
|
|
60
|
+
if (cast.eventModel) url('eventModel.repo', cast.eventModel.repo);
|
|
61
|
+
for (const event of cast.careerEvents) if (event.orgUrl) url(`career.${event.title}.orgUrl`, event.orgUrl);
|
|
62
|
+
for (const pullRequest of github.mergedPullRequests.latest) url(`github.pullRequest.${pullRequest.number}`, pullRequest.url);
|
|
63
|
+
for (const [name, release] of Object.entries(github.releases)) url(`github.releases.${name}`, release.url);
|
|
64
|
+
if (github.profile?.avatarUrl) url('github.avatarUrl', github.profile.avatarUrl);
|
|
65
|
+
for (const source of cast.writing.sources) url('writing.sources', source);
|
|
66
|
+
for (const post of posts) url(`writing.posts.${post.title}`, post.url);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function texts(cast: CastView): void {
|
|
70
|
+
const { person, cratis, agentic, eventModel } = cast;
|
|
71
|
+
for (const [what, text] of [['tagline', person.tagline], ['intro', person.intro], ['cratis.pitch', cratis?.pitch], ['cratis.role', cratis?.role], ['agentic.pitch', agentic?.pitch], ['eventModel.intro', eventModel?.intro]] as const) inline(what, text);
|
|
72
|
+
person.about.forEach((text, index) => inline(`about.${index}`, text));
|
|
73
|
+
cast.careerEvents.forEach((event, index) => inline(`career.${index}.summary`, event.summary));
|
|
74
|
+
cast.projects.forEach((project, index) => inline(`projects.${index}.pitch`, project.pitch));
|
|
75
|
+
cast.talks.forEach((talk, index) => inline(`talks.${index}.summary`, talk.summary));
|
|
76
|
+
cast.cratisRepos.forEach((repo, index) => inline(`cratis.repos.${index}.desc`, repo.desc));
|
|
77
|
+
agentic?.workflow.forEach((step, index) => inline(`agentic.workflow.${index}`, step.text));
|
|
78
|
+
agentic?.items.forEach((item, index) => inline(`agentic.items.${index}`, item.text));
|
|
79
|
+
eventModel?.cards.forEach((card, index) => inline(`eventModel.cards.${index}`, card.note));
|
|
80
|
+
eventModel?.steps.forEach((step, index) => inline(`eventModel.steps.${index}`, step.text));
|
|
81
|
+
cast.sections.forEach((section, index) => {
|
|
82
|
+
inline(`sections.${index}.title`, section.title);
|
|
83
|
+
inline(`sections.${index}.intro`, section.intro);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The career replay and the CV fold every event in order: each position must reduce to dates they can show. */
|
|
88
|
+
function career(cast: CastView): void {
|
|
89
|
+
let state = initialProjection;
|
|
90
|
+
for (const event of cast.careerEvents) {
|
|
91
|
+
if (!isCalendarMonth(event.date)) fail(`career.${event.title}.date`, event.date);
|
|
92
|
+
if (event.endDate !== undefined && !isCalendarMonth(event.endDate)) fail(`career.${event.title}.endDate`, event.endDate);
|
|
93
|
+
state = apply(state, event);
|
|
94
|
+
alsoText(state);
|
|
95
|
+
if (!isCalendarMonth(normalizeDate(event.date))) fail(`career.${event.title}.date`, event.date);
|
|
96
|
+
}
|
|
97
|
+
for (const role of state.roles) [role.from, role.to].forEach(date => date !== undefined && shown(`career.roles.${role.org}`, formatMonth(date), date));
|
|
98
|
+
if (!Number.isFinite(yearsBetween(state.shippingSince, state.clock))) fail('career.years', state.clock);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function calendar(cast: CastView): void {
|
|
102
|
+
const { github } = cast;
|
|
103
|
+
for (const week of github.contributions.weeks) {
|
|
104
|
+
if (!isCalendarDate(week.start)) fail('github.contributions.weeks.start', week.start);
|
|
105
|
+
week.counts.forEach(value => count('github.contributions.weeks.counts', value));
|
|
106
|
+
}
|
|
107
|
+
const folded = foldCalendar(github.contributions.weeks);
|
|
108
|
+
for (const day of [folded.first, folded.last, folded.busiest.date].filter(Boolean)) shown('github.contributions', formatDay(day), day);
|
|
109
|
+
for (const { month } of folded.months) shown('github.contributions.months', formatMonthLong(month), month);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function dates(cast: CastView): void {
|
|
113
|
+
const { github } = cast;
|
|
114
|
+
for (const [what, value] of [['github.fetchedAt', github.fetchedAt], ['writing.fetchedAt', cast.blogFetchedAt]] as const) {
|
|
115
|
+
if (value !== undefined && !isInstant(value)) fail(what, value);
|
|
116
|
+
}
|
|
117
|
+
shown('snapshotDate', cast.snapshotDate(), String(github.fetchedAt));
|
|
118
|
+
for (const pullRequest of github.mergedPullRequests.latest) if (!isInstant(pullRequest.mergedAt)) fail('github.pullRequest.mergedAt', pullRequest.mergedAt);
|
|
119
|
+
for (const release of Object.values(github.releases)) if (!isInstant(release.publishedAt)) fail('github.releases.publishedAt', release.publishedAt);
|
|
120
|
+
for (const post of cast.posts) if (!isInstant(post.date)) fail('writing.posts.date', post.date);
|
|
121
|
+
for (const entry of [...cast.talks, ...cast.community]) if (!isPartialDate(entry.date)) fail('talks.date', entry.date);
|
|
122
|
+
for (const value of Object.values(cast.now)) if (typeof value === 'string' && /^\d{4}-\d{2}$/.test(value)) shown('now', formatMonth(value), value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function counts(cast: CastView): void {
|
|
126
|
+
const { github } = cast;
|
|
127
|
+
count('github.contributions.total', github.contributions.total);
|
|
128
|
+
count('github.contributions.commits', github.contributions.commits);
|
|
129
|
+
count('github.mergedPullRequests.total', github.mergedPullRequests.total);
|
|
130
|
+
for (const value of Object.values(github.mergedPullRequests.byOrg)) count('github.mergedPullRequests.byOrg', value);
|
|
131
|
+
for (const standing of Object.values(github.repositories)) for (const value of Object.values(standing)) count('github.repositories', value);
|
|
132
|
+
for (const value of Object.values(github.stars)) count('github.stars', value);
|
|
133
|
+
count('mergedPrTotal', cast.mergedPrTotal);
|
|
134
|
+
for (const stat of cast.stats) if (!Number.isFinite(stat.value)) fail(`stats.${stat.label}`, stat.value);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Computes what the profile components compute from a view (URLs, the contribution calendar, the career
|
|
139
|
+
* projection, dates, inline markup and the browser data), without rendering. Throws on the first value they
|
|
140
|
+
* could not render, so a builder can refuse that data before a page fails the whole build.
|
|
141
|
+
*/
|
|
142
|
+
export function assertRenderable(cast: CastView): void {
|
|
143
|
+
links(cast);
|
|
144
|
+
texts(cast);
|
|
145
|
+
career(cast);
|
|
146
|
+
calendar(cast);
|
|
147
|
+
dates(cast);
|
|
148
|
+
counts(cast);
|
|
149
|
+
accentTokens(cast.config.theme.accent);
|
|
150
|
+
safeJson(clientCast(cast));
|
|
151
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { CareerEvent } from '../../config/CastConfig';
|
|
5
|
+
import type { PullRequest } from '../../github/GitHubSnapshot';
|
|
6
|
+
import type { WritingTheme } from '../lib/blog';
|
|
7
|
+
import { anchorFor } from '../lib/sections';
|
|
8
|
+
import type { CastView, ProfileLink } from './CastView';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What the browser scripts (live feed, replay, palette, terminal) read about the profile on the page. The
|
|
12
|
+
* layout embeds it as a JSON island, so the same scripts serve every profile the directory renders.
|
|
13
|
+
*/
|
|
14
|
+
export type ClientCast = {
|
|
15
|
+
/** Path of the profile's home page, ending with '/'. */
|
|
16
|
+
base: string;
|
|
17
|
+
verificationUrl: string;
|
|
18
|
+
/** rel for links from the member's data. */
|
|
19
|
+
rel: string;
|
|
20
|
+
login: string;
|
|
21
|
+
orgs: string[];
|
|
22
|
+
feedOrg: string;
|
|
23
|
+
person: { name: string; handle: string; title: string; location?: string; tagline: string; email?: string };
|
|
24
|
+
firstName: string;
|
|
25
|
+
now: Record<string, string | string[]>;
|
|
26
|
+
careerEvents: CareerEvent[];
|
|
27
|
+
skills: Record<string, string[]>;
|
|
28
|
+
talks: { date: string; title: string; host: string; url?: string }[];
|
|
29
|
+
profileLinks: ProfileLink[];
|
|
30
|
+
cratis?: { pitch?: string; links: Record<string, string>; repos: { name: string; role: string; url?: string; desc: string }[] };
|
|
31
|
+
eventModelName?: string;
|
|
32
|
+
/** DOM ids of the sections that render, in page order. */
|
|
33
|
+
sections: string[];
|
|
34
|
+
projects: { name: string; url: string; pitch: string; version?: string; category: string; features: string[]; stack: string[]; install?: string }[];
|
|
35
|
+
pullRequests: PullRequest[];
|
|
36
|
+
mergedPrTotal: number;
|
|
37
|
+
snapshotDate: string;
|
|
38
|
+
posts: { headline: string; url: string; part?: number; minutes?: number; themes: string[] }[];
|
|
39
|
+
themes: WritingTheme[];
|
|
40
|
+
blogHost: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** The client data for one profile view. */
|
|
44
|
+
export function clientCast(cast: CastView): ClientCast {
|
|
45
|
+
const { person } = cast;
|
|
46
|
+
return {
|
|
47
|
+
base: cast.host.base,
|
|
48
|
+
verificationUrl: cast.verificationUrl,
|
|
49
|
+
rel: cast.host.rel,
|
|
50
|
+
login: cast.config.github.login,
|
|
51
|
+
orgs: cast.config.github.orgs,
|
|
52
|
+
feedOrg: cast.feedOrg,
|
|
53
|
+
person: {
|
|
54
|
+
name: person.name,
|
|
55
|
+
handle: person.handle,
|
|
56
|
+
title: person.title,
|
|
57
|
+
tagline: person.tagline,
|
|
58
|
+
...(person.location ? { location: person.location } : {}),
|
|
59
|
+
...(person.email ? { email: person.email } : {}),
|
|
60
|
+
},
|
|
61
|
+
firstName: cast.firstName,
|
|
62
|
+
now: cast.now,
|
|
63
|
+
careerEvents: cast.careerEvents,
|
|
64
|
+
skills: cast.skills,
|
|
65
|
+
talks: cast.talks.map(talk => ({ date: talk.date, title: talk.title, host: talk.host, ...(talk.url ? { url: talk.url } : {}) })),
|
|
66
|
+
profileLinks: cast.profileLinks,
|
|
67
|
+
...(cast.cratis ? {
|
|
68
|
+
cratis: {
|
|
69
|
+
...(cast.cratis.pitch ? { pitch: cast.cratis.pitch } : {}),
|
|
70
|
+
links: cast.cratis.links,
|
|
71
|
+
repos: cast.cratisRepos.map(repo => ({ name: repo.name, role: repo.role, desc: repo.desc, ...(repo.url ? { url: repo.url } : {}) })),
|
|
72
|
+
},
|
|
73
|
+
} : {}),
|
|
74
|
+
...(cast.eventModel ? { eventModelName: cast.eventModel.name } : {}),
|
|
75
|
+
sections: cast.sections.map(section => anchorFor[section.id]),
|
|
76
|
+
projects: cast.liveProjects.map(project => ({
|
|
77
|
+
name: project.name,
|
|
78
|
+
url: project.url,
|
|
79
|
+
pitch: project.pitch,
|
|
80
|
+
category: project.category,
|
|
81
|
+
features: project.features,
|
|
82
|
+
stack: project.stack,
|
|
83
|
+
...(project.version ? { version: project.version } : {}),
|
|
84
|
+
...(project.install ? { install: project.install } : {}),
|
|
85
|
+
})),
|
|
86
|
+
pullRequests: cast.github.mergedPullRequests.latest,
|
|
87
|
+
mergedPrTotal: cast.mergedPrTotal,
|
|
88
|
+
snapshotDate: cast.snapshotDate(),
|
|
89
|
+
posts: cast.posts.map(post => ({
|
|
90
|
+
headline: post.headline,
|
|
91
|
+
url: post.url,
|
|
92
|
+
themes: post.themes,
|
|
93
|
+
...(post.part !== undefined ? { part: post.part } : {}),
|
|
94
|
+
...(post.minutes !== undefined ? { minutes: post.minutes } : {}),
|
|
95
|
+
})),
|
|
96
|
+
themes: cast.themes,
|
|
97
|
+
blogHost: cast.blogHost,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let cachedText: string | undefined;
|
|
102
|
+
let cachedValue: ClientCast | undefined;
|
|
103
|
+
|
|
104
|
+
/** Reads the client data of the page currently shown. Read again on every use: the router swaps pages. */
|
|
105
|
+
export function readClientCast(): ClientCast | undefined {
|
|
106
|
+
const text = document.querySelector('script[data-cast-client]')?.textContent ?? undefined;
|
|
107
|
+
if (!text) return undefined;
|
|
108
|
+
if (text !== cachedText) {
|
|
109
|
+
try {
|
|
110
|
+
cachedValue = JSON.parse(text) as ClientCast;
|
|
111
|
+
cachedText = text;
|
|
112
|
+
} catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return cachedValue;
|
|
117
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
export * from './assertRenderable';
|
|
5
|
+
export * from './CastHost';
|
|
6
|
+
export * from './CastView';
|
|
7
|
+
export * from './clientCast';
|
|
8
|
+
export { parseInline, plainText, type InlineToken } from '../lib/inline';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
// Small page-wide behaviours: reveal on scroll, pointer glow, copy buttons, nav state.
|
|
5
|
+
import { motionReduced, onMotionChange } from '../lib/motion';
|
|
6
|
+
import { copyText } from '../lib/clipboard';
|
|
7
|
+
import { onPageLoad, disposer } from '../lib/lifecycle';
|
|
8
|
+
|
|
9
|
+
// --- Pointer-following glow ---------------------------------------------------
|
|
10
|
+
document.addEventListener(
|
|
11
|
+
'pointermove',
|
|
12
|
+
(e) => {
|
|
13
|
+
const target = (e.target as Element | null)?.closest<HTMLElement>('.glow');
|
|
14
|
+
if (!target) return;
|
|
15
|
+
const r = target.getBoundingClientRect();
|
|
16
|
+
target.style.setProperty('--mx', `${e.clientX - r.left}px`);
|
|
17
|
+
target.style.setProperty('--my', `${e.clientY - r.top}px`);
|
|
18
|
+
},
|
|
19
|
+
{ passive: true },
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// --- Copy to clipboard --------------------------------------------------------
|
|
23
|
+
document.addEventListener('click', async (e) => {
|
|
24
|
+
const btn = (e.target as Element | null)?.closest<HTMLButtonElement>('button[data-copy]');
|
|
25
|
+
if (!btn) return;
|
|
26
|
+
const text = btn.dataset['copy'] ?? '';
|
|
27
|
+
const ok = await copyText(text);
|
|
28
|
+
const label = btn.querySelector<HTMLElement>('[data-copy-label]');
|
|
29
|
+
const status = btn.parentElement?.querySelector<HTMLElement>('[data-copy-status]');
|
|
30
|
+
const original = label?.textContent ?? '';
|
|
31
|
+
btn.dataset['copied'] = ok ? 'true' : 'false';
|
|
32
|
+
if (label) label.textContent = ok ? 'Copied' : 'Press ⌘C';
|
|
33
|
+
if (status) status.textContent = ok ? `Copied ${text} to the clipboard` : 'Copy failed';
|
|
34
|
+
window.setTimeout(() => {
|
|
35
|
+
delete btn.dataset['copied'];
|
|
36
|
+
if (label) label.textContent = original;
|
|
37
|
+
}, 1600);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// --- Per page view: reveal on scroll and nav state ------------------------------
|
|
41
|
+
onPageLoad(() => {
|
|
42
|
+
const d = disposer();
|
|
43
|
+
|
|
44
|
+
const revealables = document.querySelectorAll<HTMLElement>('[data-reveal]');
|
|
45
|
+
const revealAll = () => revealables.forEach((el) => el.classList.add('is-visible'));
|
|
46
|
+
if (motionReduced() || !('IntersectionObserver' in window)) {
|
|
47
|
+
revealAll();
|
|
48
|
+
} else {
|
|
49
|
+
const io = new IntersectionObserver(
|
|
50
|
+
(entries) => {
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (entry.isIntersecting) {
|
|
53
|
+
entry.target.classList.add('is-visible');
|
|
54
|
+
io.unobserve(entry.target);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{ rootMargin: '0px 0px -8% 0px', threshold: 0.08 },
|
|
59
|
+
);
|
|
60
|
+
revealables.forEach((el) => io.observe(el));
|
|
61
|
+
d.add(() => io.disconnect());
|
|
62
|
+
}
|
|
63
|
+
d.add(onMotionChange((reduced) => reduced && revealAll()));
|
|
64
|
+
|
|
65
|
+
const nav = document.querySelector<HTMLElement>('[data-nav]');
|
|
66
|
+
if (nav) {
|
|
67
|
+
const update = () => nav.toggleAttribute('data-scrolled', window.scrollY > 8);
|
|
68
|
+
update();
|
|
69
|
+
d.on(window, 'scroll', update, { passive: true });
|
|
70
|
+
|
|
71
|
+
const links = nav.querySelectorAll<HTMLAnchorElement>('a[data-section]');
|
|
72
|
+
const sections = [...links]
|
|
73
|
+
.map((a) => document.getElementById(a.dataset['section'] ?? ''))
|
|
74
|
+
.filter((s): s is HTMLElement => !!s);
|
|
75
|
+
if (sections.length) {
|
|
76
|
+
const spy = new IntersectionObserver(
|
|
77
|
+
(entries) => {
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (!entry.isIntersecting) continue;
|
|
80
|
+
links.forEach((a) => {
|
|
81
|
+
if (a.dataset['section'] === entry.target.id) a.setAttribute('aria-current', 'location');
|
|
82
|
+
else a.removeAttribute('aria-current');
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{ rootMargin: '-45% 0px -50% 0px' },
|
|
87
|
+
);
|
|
88
|
+
sections.forEach((s) => spy.observe(s));
|
|
89
|
+
d.add(() => spy.disconnect());
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return d.dispose;
|
|
93
|
+
});
|