@panaversity/ksor 0.0.30 → 0.0.31
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/CHANGELOG.md +67 -0
- package/README.md +24 -0
- package/dist/cli.mjs +7 -3
- package/dist/{gateway-api-CbFkHZiU-HvlJRjRB.mjs → gateway-api-6nC9x54K-BWFTI_6U.mjs} +1 -1
- package/dist/gateway.mjs +1 -1
- package/package.json +1 -1
- package/templates/scaffold/.agents/skills/format-checker/check.mjs +9 -1
- package/templates/scaffold/.agents/skills/make-slides/SKILL.md +160 -0
- package/templates/scaffold/.claude/skills/format-checker/check.mjs +9 -1
- package/templates/scaffold/.claude/skills/make-slides/SKILL.md +160 -0
- package/templates/scaffold/AGENTS.md +45 -5
- package/templates/scaffold/README.md +24 -1
- package/templates/scaffold/knowledge/what-is-a-ksor.slides.yaml +65 -0
- package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +9 -1
- package/templates/scaffold/system/site/app/global.css +45 -0
- package/templates/scaffold/system/site/components/deck-viewer.tsx +195 -0
- package/templates/scaffold/system/site/components/slides.tsx +128 -0
- package/templates/scaffold/system/site/lib/attachment-rule.ts +7 -0
- package/templates/scaffold/system/site/lib/attachments.ts +49 -2
- package/templates/scaffold/system/site/lib/slides-embed.ts +93 -0
- package/templates/scaffold/system/site/lib/slides.ts +123 -0
- package/templates/scaffold/system/site/source.config.ts +9 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a presentation's share link into the url that can be framed.
|
|
3
|
+
*
|
|
4
|
+
* Every deck host publishes two different urls — the one you get from "Share"
|
|
5
|
+
* and the one that works in an `<iframe>` — and an author will paste the first.
|
|
6
|
+
* Deriving the second is a small rule per provider, and getting it wrong is
|
|
7
|
+
* invisible until someone opens the page, so it is a table with tests rather
|
|
8
|
+
* than a regex written once.
|
|
9
|
+
*
|
|
10
|
+
* A LEAF: no imports. `slides.ts` carries zod and cannot enter the unit tier.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Only https. A browser blocks an http frame inside a secure page as mixed
|
|
15
|
+
* content, so an http embed publishes a panel that silently never loads —
|
|
16
|
+
* which is worse than refusing it, because nothing goes red.
|
|
17
|
+
*/
|
|
18
|
+
export function isHttpsUrl(value: string): boolean {
|
|
19
|
+
try {
|
|
20
|
+
return new URL(value).protocol === "https:";
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One provider's rule: which hosts it owns, and how a share url becomes an
|
|
28
|
+
* embed url. Extend this rather than special-casing at a call site.
|
|
29
|
+
*/
|
|
30
|
+
interface Provider {
|
|
31
|
+
readonly label: string;
|
|
32
|
+
readonly hosts: readonly string[];
|
|
33
|
+
readonly embed: (url: URL) => string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PROVIDERS: readonly Provider[] = [
|
|
37
|
+
{
|
|
38
|
+
label: "Google Slides",
|
|
39
|
+
hosts: ["docs.google.com"],
|
|
40
|
+
// /presentation/d/<id>/edit -> /presentation/d/<id>/embed
|
|
41
|
+
embed: (url) => {
|
|
42
|
+
const match = /^\/presentation\/d\/([^/]+)/.exec(url.pathname);
|
|
43
|
+
if (match === null) return null;
|
|
44
|
+
return `https://docs.google.com/presentation/d/${match[1]}/embed?start=false&loop=false&delayms=3000`;
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: "Canva",
|
|
49
|
+
hosts: ["www.canva.com", "canva.com"],
|
|
50
|
+
// /design/<id>/<token>/view -> same, with ?embed
|
|
51
|
+
embed: (url) => {
|
|
52
|
+
const match = /^\/design\/([^/]+)\/([^/]+)/.exec(url.pathname);
|
|
53
|
+
if (match === null) return null;
|
|
54
|
+
return `https://www.canva.com/design/${match[1]}/${match[2]}/view?embed`;
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
label: "SlideShare",
|
|
59
|
+
hosts: ["www.slideshare.net", "slideshare.net"],
|
|
60
|
+
// Only an explicit /slideshow/embed_code/ url is framable.
|
|
61
|
+
embed: (url) => (url.pathname.startsWith("/slideshow/embed_code/") ? url.toString() : null),
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/** The provider's own name for a url, or null when it is not one we know. */
|
|
66
|
+
export function providerOf(value: string): string | null {
|
|
67
|
+
try {
|
|
68
|
+
const url = new URL(value);
|
|
69
|
+
return PROVIDERS.find((p) => p.hosts.includes(url.hostname))?.label ?? null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The url to put in the frame, or null when this one cannot be derived.
|
|
77
|
+
*
|
|
78
|
+
* Null is an ordinary answer, not an error: the deck still renders as a link,
|
|
79
|
+
* and an author who wants a frame supplies `embed:` explicitly.
|
|
80
|
+
*/
|
|
81
|
+
export function embedUrlFor(value: string): string | null {
|
|
82
|
+
try {
|
|
83
|
+
const url = new URL(value);
|
|
84
|
+
if (url.protocol !== "https:") return null;
|
|
85
|
+
const provider = PROVIDERS.find((p) => p.hosts.includes(url.hostname));
|
|
86
|
+
return provider?.embed(url) ?? null;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Every provider whose share url this understands, for docs and errors. */
|
|
93
|
+
export const KNOWN_PROVIDERS: readonly string[] = PROVIDERS.map((p) => p.label);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { embedUrlFor, isHttpsUrl } from "./slides-embed";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The shape of a `<doc>.slides.yaml` — a presentation that teaches this
|
|
7
|
+
* document.
|
|
8
|
+
*
|
|
9
|
+
* The predecessor authors this INLINE in MDX: a `## 📚 Teaching Aid` heading,
|
|
10
|
+
* a `:::tip` with the link, and a raw `<div style={{…}}>` wrapping an
|
|
11
|
+
* `<iframe>` (`specs/crashcourses/connector-native-apps/…md`). None of that is
|
|
12
|
+
* available here and the reason is critical rule 2: `knowledge/` is CommonMark
|
|
13
|
+
* only, so a document cannot carry raw JSX and stay readable in a plain
|
|
14
|
+
* markdown viewer. The deck is an attachment instead, which also means it
|
|
15
|
+
* inherits the document's tier and takedown rather than being an embed nobody
|
|
16
|
+
* governs.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* One slide, as the record carries it.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately NOT freeform markdown or HTML. A slide is a heading and a few
|
|
22
|
+
* lines, and admitting arbitrary markup would put layout into the record —
|
|
23
|
+
* which is the same mistake as embedding a deck, one level down. What a slide
|
|
24
|
+
* says is knowledge; how it looks is the site's business.
|
|
25
|
+
*/
|
|
26
|
+
export const SlideSchema = z.object({
|
|
27
|
+
heading: z.string().min(1).max(120),
|
|
28
|
+
/**
|
|
29
|
+
* Three to five in practice. The cap is six because a slide someone reads
|
|
30
|
+
* aloud is a slide nobody listens to, and a limit is the only thing that
|
|
31
|
+
* reliably stops a generator from pasting a paragraph per slide.
|
|
32
|
+
*/
|
|
33
|
+
bullets: z.array(z.string().min(1).max(240)).max(6).optional(),
|
|
34
|
+
/** One line under the heading, for a slide that makes a single point. */
|
|
35
|
+
lead: z.string().max(300).optional(),
|
|
36
|
+
/** Spoken, not shown. Rendered for the presenter, never on the slide. */
|
|
37
|
+
note: z.string().max(600).optional(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const SlidesSchema = z
|
|
41
|
+
.object({
|
|
42
|
+
slides: z.object({
|
|
43
|
+
title: z.string().min(1).max(120),
|
|
44
|
+
/**
|
|
45
|
+
* Where the deck lives. HTTPS only — an http:// embed is blocked as
|
|
46
|
+
* mixed content on any deployed site, so accepting one would publish a
|
|
47
|
+
* frame that silently never loads.
|
|
48
|
+
*/
|
|
49
|
+
url: z.string().url().max(2000).optional(),
|
|
50
|
+
/**
|
|
51
|
+
* The provider's EMBED url, when it differs from the share url. Optional
|
|
52
|
+
* because `embedUrlFor` derives it for providers whose rule is known;
|
|
53
|
+
* required in practice for any provider whose is not, and a deck with
|
|
54
|
+
* neither renders as a link rather than as a broken frame.
|
|
55
|
+
*/
|
|
56
|
+
embed: z.string().url().max(2000).optional(),
|
|
57
|
+
/** Shown beside the link — "Google Slides", "Canva". Never inferred. */
|
|
58
|
+
provider: z.string().max(60).optional(),
|
|
59
|
+
/** One line under the title, if the deck needs introducing. */
|
|
60
|
+
description: z.string().max(300).optional(),
|
|
61
|
+
}),
|
|
62
|
+
/**
|
|
63
|
+
* The slides themselves, when the record carries them.
|
|
64
|
+
*
|
|
65
|
+
* This is the mode that makes the workflow complete: an agent writes these
|
|
66
|
+
* from the document with no browser and no third party, and the site
|
|
67
|
+
* renders the presentation. The deck is then governed like everything else
|
|
68
|
+
* here — reviewed in a PR, versioned with its document, withdrawn with it —
|
|
69
|
+
* and it cannot rot into a dead link, because there is no link.
|
|
70
|
+
*/
|
|
71
|
+
deck: z.array(SlideSchema).min(1).max(60).optional(),
|
|
72
|
+
})
|
|
73
|
+
.superRefine((value, ctx) => {
|
|
74
|
+
// Exactly one source. A deck that is BOTH authored here and embedded from
|
|
75
|
+
// elsewhere has two versions and no answer to which one governs — which is
|
|
76
|
+
// the disagreement this whole product exists to settle.
|
|
77
|
+
const authored = (value.deck?.length ?? 0) > 0;
|
|
78
|
+
const linked = value.slides.url !== undefined;
|
|
79
|
+
if (!authored && !linked) {
|
|
80
|
+
ctx.addIssue({
|
|
81
|
+
code: "custom",
|
|
82
|
+
path: ["deck"],
|
|
83
|
+
message:
|
|
84
|
+
"ksor-slides-empty: a presentation needs either `deck:` (slides the record carries) or `slides.url:` (a deck hosted elsewhere) — with neither there is nothing to show",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (authored && linked) {
|
|
88
|
+
ctx.addIssue({
|
|
89
|
+
code: "custom",
|
|
90
|
+
path: ["deck"],
|
|
91
|
+
message:
|
|
92
|
+
"ksor-slides-two-sources: this declares both `deck:` and `slides.url:`, so there are two presentations and nothing says which one governs — keep the one the record owns, or drop `deck:` and keep the link",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const key of ["url", "embed"] as const) {
|
|
97
|
+
const candidate = value.slides[key];
|
|
98
|
+
if (candidate !== undefined && !isHttpsUrl(candidate)) {
|
|
99
|
+
ctx.addIssue({
|
|
100
|
+
code: "custom",
|
|
101
|
+
path: ["slides", key],
|
|
102
|
+
message: `ksor-slides-insecure: ${key} must be https:// — a browser blocks an http:// frame on a secure page as mixed content, so this would publish a deck that silently never loads`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// A deck nobody can embed is still a legitimate deck — it renders as a
|
|
107
|
+
// link. But a deck we cannot embed AND whose provider we cannot name is
|
|
108
|
+
// worth telling the author about, because they probably expected a frame.
|
|
109
|
+
if (
|
|
110
|
+
linked &&
|
|
111
|
+
value.slides.embed === undefined &&
|
|
112
|
+
embedUrlFor(value.slides.url ?? "") === null
|
|
113
|
+
) {
|
|
114
|
+
ctx.addIssue({
|
|
115
|
+
code: "custom",
|
|
116
|
+
path: ["slides", "embed"],
|
|
117
|
+
message: `ksor-slides-no-embed: this url has no embed form ksor knows how to derive, so the deck would render as a link only — add an explicit \`embed:\` url if you want it shown inline`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export type Slides = z.infer<typeof SlidesSchema>;
|
|
123
|
+
export type Slide = z.infer<typeof SlideSchema>;
|
|
@@ -3,6 +3,7 @@ import { metaSchema, pageSchema } from "fumadocs-core/source/schema";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { DeckSchema } from "./lib/deck";
|
|
5
5
|
import { QuizSchema } from "./lib/quiz";
|
|
6
|
+
import { SlidesSchema } from "./lib/slides";
|
|
6
7
|
import { knowledgeSourceDir } from "./lib/stage-knowledge";
|
|
7
8
|
|
|
8
9
|
// The record lives at <repo>/knowledge — two levels up from this site.
|
|
@@ -103,6 +104,14 @@ export const quizzes = defineCollections({
|
|
|
103
104
|
schema: QuizSchema,
|
|
104
105
|
});
|
|
105
106
|
|
|
107
|
+
/** The presentation that teaches a document — see components/slides.tsx. */
|
|
108
|
+
export const slides = defineCollections({
|
|
109
|
+
type: "meta",
|
|
110
|
+
dir: knowledgeSourceDir(),
|
|
111
|
+
files: ["**/*.slides.yaml"],
|
|
112
|
+
schema: SlidesSchema,
|
|
113
|
+
});
|
|
114
|
+
|
|
106
115
|
export default defineConfig({
|
|
107
116
|
mdxOptions: {
|
|
108
117
|
// MDX options
|