diff --git a/COPY.md b/COPY.md index 4bd7de0e..cd2bd791 100644 --- a/COPY.md +++ b/COPY.md @@ -44,6 +44,8 @@ These exact strings. Do not paraphrase them per page. | Demos page | See what skills make · Each demo is one recorded run: the prompt, and what the Agent built with the Skill. Pick a prompt to see the output, or open it live. | guidance only: the `/skills/demos` H1 and line. The meta title is "Claude skill examples: see what each one makes" for the admitted query | | Demo page | What /{name} made · One recorded run: the prompt, and what the Agent built with the Skill. | guidance only: the H1 and line of a demo page, `/skills/demos///`. The meta title is "{name} skill example: {noun}" for the query " skill example", with `noun` from `DEMO_GROUPS`. The meta description is "{agent} made this {noun} with the /{name} skill from one prompt: “{prompt}”", cut at a word to 160 characters | | Demo groups | Films and launch videos · Landing pages · UI components · Diagrams and explainers | guidance only: `DEMO_GROUPS` in `shared/demo-groups.ts`: the `/skills/demos` group headings and their lines | +| Writing demo | Writing · Articles, READMEs, and PR descriptions. · Files · Updated · Diff · Markdown · No newline at end of file | guidance only: writing support on the existing demos board. Show file buttons with Markdown icons and one Diff toggle with an icon. Show the updated document by default. Diff compares the original seed and updated Markdown with line numbers and syntax highlighting. Fit short documents to their content and cap long documents. Keep the no-Skill baseline in recording data for later evaluations. No decorative text. | +| Writing demo example | Example data · Example data. No Skills were run. | guidance only: each known writing Skill uses `/skills/demos///`, with `/{name}` as its heading. All candidate rows share handwritten UI fixtures. Keep the disclosure visible. Show no recording metadata. Reuse the demo footer with Skill links, Run it yourself, and Copy demo link. The run prompt supplies the original fixture documents. README and PR documents use GitHub-style Markdown with the shared Skill renderer and rangi highlighter. Diff toggles the Markdown comparison on and off. | | Demo actions | Run it yourself · Agent prompt · Terminal · Copy Agent prompt · Copy command · Copied · Copy demo link · Copied demo link | guidance only: demo stages. The Agent prompt includes the recorded task. The terminal option loads the Skill. Run is hidden on phones. | | Demo stage | Open the demo | guidance only: the demo stage link. The hero shows the output, author row, and this link only. On the homepage it opens the demo page; on a demo page it opens the Skill page Demo panel | | Home demo carousel | Previous demo · Next demo · {n} of {total} | guidance only: the manual homepage demo controls. The position is live text for screen readers. | diff --git a/GLOSSARY.md b/GLOSSARY.md index 358d6dbf..7fa02240 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -118,6 +118,9 @@ It grants no trust or search indexing. Existing Skills retain source refresh. **Groups:** demos group by what the Skill makes, on `/skills/demos`: films and launch videos, landing pages, UI components, and diagrams and explainers. A group is not a track: tracks sort Skills by kind of work. +**Writing:** a demo can show Markdown documents. Each document keeps its original, the rewrite without a Skill, and the Skill output. +UI fixtures are labelled Example data. They are not recorded runs or evaluation results. + ### preview **Is:** the step of watching a Skill's demo before running it. A preview never executes a Skill. diff --git a/app/components/DemoStage.vue b/app/components/DemoStage.vue index 6ac74fb3..0eec4940 100644 --- a/app/components/DemoStage.vue +++ b/app/components/DemoStage.vue @@ -1,11 +1,11 @@ @@ -176,13 +166,6 @@ const liveUrl = computed(() => live && !demo.video ? demo.liveUrl : null) aspect-ratio: var(--phone-ratio, 4 / 5); } - .demo-stage__id :deep(.skill-card--row) { - padding-block: 0.5rem; - } - - .demo-stage__id :deep(.skill-card__row-end) { - display: none; - } } /* Give interactive pages most of the screen, while keeping their actions in reach. */ @@ -197,15 +180,6 @@ const liveUrl = computed(() => live && !demo.video ? demo.liveUrl : null) inline-size: min(100%, calc(68svh * var(--video-w) / var(--video-h))); } -.demo-stage--compact .demo-stage__id { - position: sticky; - inset-block-end: 0; - z-index: 2; - border-block-start: 1px solid var(--ui-border); - background: var(--ui-bg); - padding-block-end: env(safe-area-inset-bottom, 0px); -} - .demo-stage--compact .demo-stage__head { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -268,15 +242,4 @@ const liveUrl = computed(() => live && !demo.video ? demo.liveUrl : null) outline: 2px solid var(--ui-border-accented); outline-offset: 2px; } - -/* The Skill row's hover fill follows the rounded corners of everything around it. */ -.demo-stage__id { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: center; - gap: 0.5rem; - margin-block-start: 0.25rem; - overflow: hidden; - border-radius: var(--ui-radius); -} diff --git a/app/components/_DemoActions.vue b/app/components/_DemoActions.vue index 204c9979..54d1e1d2 100644 --- a/app/components/_DemoActions.vue +++ b/app/components/_DemoActions.vue @@ -1,9 +1,9 @@ + + + + diff --git a/app/components/home-demos/_DemoWriting.vue b/app/components/home-demos/_DemoWriting.vue new file mode 100644 index 00000000..9592756a --- /dev/null +++ b/app/components/home-demos/_DemoWriting.vue @@ -0,0 +1,279 @@ + + + + + diff --git a/app/utils/home-demos.ts b/app/utils/home-demos.ts index 0e2839d7..72cecf2e 100644 --- a/app/utils/home-demos.ts +++ b/app/utils/home-demos.ts @@ -1,6 +1,7 @@ import type { DemoMakes } from '#shared/demo-groups' import type { DemoEffort } from '#shared/demo-recording' import type { DemoTokenUsage } from '#shared/demo-usage' +import type { WritingDemo } from '#shared/writing-demo' import type { SkillCardSkill } from '~/types/skill-card' import { DEMO_GROUPS } from '#shared/demo-groups' import { demoAgentIcon, demoRecordingLabel } from '#shared/demo-recording' @@ -32,6 +33,8 @@ export interface HomeDemoItem { /** The sandboxed output page of a page demo; null for a film. */ liveUrl: string | null video: HomeDemoVideo | null + /** Markdown documents, with the original and the rewrite without a Skill. */ + writing?: WritingDemo | null } export interface HomeDemoVideo { @@ -80,7 +83,7 @@ export function homeDemoFeed(feed: { items: HomeDemoItem[] }) { */ export const DEMO_PHONE_MEDIA = '(max-width: 39.99rem)' -export function demoKey(demo: HomeDemoItem): string { +export function demoKey(demo: Pick): string { return `${demo.owner}/${demo.repo}/${demo.name}` } @@ -156,7 +159,9 @@ export function demoSocialPicture(demo: Pick): } /** The Skill as `SkillCard` reads it, so a demo names its author the way every Skill embed does. */ -export function demoCardSkill(demo: HomeDemoItem): SkillCardSkill { +export type DemoIdentity = Pick + +export function demoCardSkill(demo: DemoIdentity): SkillCardSkill { return { owner: demo.owner, repo: demo.repo, diff --git a/app/utils/writing-demo-example.ts b/app/utils/writing-demo-example.ts new file mode 100644 index 00000000..c4ba085f --- /dev/null +++ b/app/utils/writing-demo-example.ts @@ -0,0 +1,156 @@ +import type { WritingDemo } from '#shared/writing-demo' + +/** Candidates for future recordings. All rows share the same UI fixtures. */ +export const writingDemoSkills = [ + { owner: 'blader', repo: 'humanizer', name: 'humanizer' }, + { owner: 'hardikpandya', repo: 'stop-slop', name: 'stop-slop' }, + { owner: 'petergyang', repo: 'no-ai-slop', name: 'no-ai-slop' }, + { owner: 'elithrar', repo: 'dotfiles', name: 'anti-slop' }, + { owner: 'kmaida', repo: 'deslop-skills', name: 'deslop-writing' }, + { owner: 'harlan-zw', repo: 'brundlefly', name: 'im-not-a-fly' }, +] as const + +/** Handwritten UI fixtures. These are not recordings or results from any Skill. */ +export const writingDemoExample: WritingDemo = { + documents: [ + { + id: 'article', + format: 'article', + label: 'reading-list.md', + original: `# Keeping a reading list that works for you + +In today's fast-paced digital landscape, managing a reading list has become an increasingly important part of staying informed. By leveraging a thoughtful approach, you can transform a collection of links into a valuable resource for learning and growth. + +## A streamlined approach + +It's worth noting that a reading list doesn't need to be complicated. Our fictional reading-list app, Margin, stores a title, URL, and category for each link. You can filter by category and bookmark items you want to return to. Everything stays in your browser. There is no account or cloud sync. + +## Building better habits + +Furthermore, a weekly review can help you navigate the ever-growing collection of articles. Remove links you no longer want to read. Keep the ones that connect to a project you are working on. + +Ultimately, the goal is to create a seamless reading experience that empowers you to make the most of your time. A small, focused list can be more useful than an extensive collection you never open.`, + baseline: `# Keep your reading list small + +A reading list is useful when you return to it. Saving more links does not help if you never open them. + +Margin is a fictional reading-list app. It stores each link's title, URL, and category. Category filters help you find a topic, and bookmarks let you mark items to revisit. + +Your list stays in your browser. There is no account or cloud sync. + +Review the list once a week. Remove articles you no longer want to read and keep those related to your current projects. + +A short list you use is more useful than a long list you ignore.`, + output: `# A reading list you might actually read + +You save an article, then another. A month later, the list is a second inbox. + +Keep the links that help with something you're working on. Delete the ones that no longer interest you. Give the list a quick look once a week. + +Margin, our fictional reading-list app, keeps three things for each link: its title, URL, and category. Filter by category when you want a topic. Bookmark a link when you want to return to it. + +The list stays in your browser. No account, no cloud sync. If you need it on another device, that limitation matters. + +You don't need to finish the list. You need to find something worth reading when you have time.`, + }, + { + id: 'readme', + format: 'readme', + label: 'README.md', + original: `# Margin + +Margin is a fictional TypeScript package designed to streamline the management of reading lists. It provides a robust yet intuitive API for organizing links by category, enabling developers to create seamless reading experiences. + +## Getting started + +To begin leveraging Margin, import \`filterLinks\` and pass your links and the desired category. The function returns matching links in their original order. Passing an empty category returns all links. + +\`\`\`ts +import { filterLinks } from 'margin' + +const links = [ + { title: 'CSS notes', url: 'https://example.com/css', category: 'CSS' }, + { title: 'TypeScript notes', url: 'https://example.com/ts', category: 'TypeScript' }, +] + +const cssLinks = filterLinks(links, 'CSS') +\`\`\` + +## Important considerations + +It's important to note that category matching is case-sensitive. The function does not modify the input array. Margin does not fetch URLs or store links. It is solely a filtering function, making it a valuable addition to your toolkit.`, + baseline: `# Margin + +Margin is a fictional TypeScript package for filtering reading-list links by category. + +## Usage + +\`\`\`ts +import { filterLinks } from 'margin' + +const links = [ + { title: 'CSS notes', url: 'https://example.com/css', category: 'CSS' }, + { title: 'TypeScript notes', url: 'https://example.com/ts', category: 'TypeScript' }, +] + +const cssLinks = filterLinks(links, 'CSS') +\`\`\` + +Matching is case-sensitive. An empty category returns all links. Results keep their original order, and the input array is not modified. + +Margin does not fetch URLs or store links.`, + output: `# Margin + +Filter a reading list by category. This is a fictional TypeScript package used for this demo. + +\`\`\`ts +import { filterLinks } from 'margin' + +const links = [ + { title: 'CSS notes', url: 'https://example.com/css', category: 'CSS' }, + { title: 'TypeScript notes', url: 'https://example.com/ts', category: 'TypeScript' }, +] + +const cssLinks = filterLinks(links, 'CSS') +\`\`\` + +\`filterLinks(links, category)\` returns matching links in their original order. It leaves the input array alone. + +- Categories are case-sensitive. \`CSS\` and \`css\` differ. +- An empty category returns all links. +- The function does not fetch URLs or store links.`, + }, + { + id: 'pr', + format: 'pr', + label: 'pr.md', + original: `# Add category filtering to the reading list + +This pull request introduces a streamlined category filtering experience to our fictional reading-list app, Margin. By leveraging the existing category field, users can now navigate their saved articles with greater ease and efficiency. + +Previously, the list displayed every saved link regardless of the selected category. With this enhancement, selecting a category displays only matching links. Selecting All displays the complete list. + +It's worth noting that filtering preserves the original order of links and does not change bookmarks. The selected category resets when the page reloads. Search and saved filter preferences are outside the scope of this change. + +Ultimately, this improvement provides a more focused and intuitive reading experience while maintaining the simplicity of the existing interface.`, + baseline: `# Add category filtering to the reading list + +Margin is a fictional reading-list app. Its list previously showed every saved link, regardless of the selected category. + +Selecting a category now shows matching links. Selecting All shows the complete list. + +Filtering preserves link order and bookmarks. The selected category resets on reload. + +This change does not include search or saved filter preferences.`, + output: `# Add category filtering to the reading list + +Selecting a category in Margin now shows its links. Previously, the list showed every link regardless of the selection. All restores the full list. + +Link order and bookmarks stay the same. Reloading the page resets the selected category. + +Search and saved filter preferences are outside this change. + +Margin is a fictional app used for this demo.`, + }, + ], +} diff --git a/layers/marketing/app/pages/skills/demos/[...demo].vue b/layers/marketing/app/pages/skills/demos/[...demo].vue index 03ceb191..301a745c 100644 --- a/layers/marketing/app/pages/skills/demos/[...demo].vue +++ b/layers/marketing/app/pages/skills/demos/[...demo].vue @@ -1,9 +1,12 @@
- +
+ + +
+
-

+

No demos are published yet.

diff --git a/layers/registry/app/components/_SkillDemo.vue b/layers/registry/app/components/_SkillDemo.vue index 6143b7ad..27e935cb 100644 --- a/layers/registry/app/components/_SkillDemo.vue +++ b/layers/registry/app/components/_SkillDemo.vue @@ -1,5 +1,6 @@ \n```') + expect(html).toContain('class="shj-kwd"') + expect(html).toContain('>const') + expect(html).toContain('<script>alert(1)</script>') + expect(html).not.toContain('\n\n\n\n::danger\n::\n\n[Run](javascript:alert%281%29)\n\n[Encoded](javascript:alert%281%29)\n\n![Image](https://tracker.example/image)') + expect(html).not.toMatch(/ { + it('compares Markdown lines and keeps both file line numbers', () => { + const rows = writingDiff('# Title\n\nOld copy\n', '# Title\n\nNew copy\n') + expect(rows.map(({ kind, originalLine, updatedLine, text }) => ({ kind, originalLine, updatedLine, text }))).toEqual([ + { kind: 'context', originalLine: 1, updatedLine: 1, text: '# Title\n' }, + { kind: 'context', originalLine: 2, updatedLine: 2, text: '\n' }, + { kind: 'removed', originalLine: 3, updatedLine: null, text: 'Old copy\n' }, + { kind: 'added', originalLine: null, updatedLine: 3, text: 'New copy\n' }, + ]) + }) + + it.each([ + ['', '# Added'], + ['# Removed', ''], + ['same\n', 'same'], + ['one\r\ntwo\r\n', 'one\r\nthree\r\n'], + ])('preserves source text, including final newline changes', (original, updated) => { + const rows = writingDiff(original, updated) + expect(rows.filter(row => row.kind !== 'added').map(row => row.text).join('')).toBe(original) + expect(rows.filter(row => row.kind !== 'removed').map(row => row.text).join('')).toBe(updated) + }) + + it('highlights Markdown and fenced code without executing HTML', () => { + const rows = writingDiff('', '# Title\n\n```ts\nconst value = "