Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ export default defineConfig({
sitemap({
// /pricing is not public yet -- it exists only so the URL resolves for
// Creem's validation. Drop this exclusion when Pro launches.
filter: (page) => !page.includes('/404') && !page.includes('/pricing'),
// /pricing and /compatibility are not public yet. Drop each exclusion
// when it launches -- /compatibility waits on the v2 server.
filter: (page) =>
!page.includes('/404') &&
!page.includes('/pricing') &&
!page.includes('/compatibility'),
serialize(item) {
// Under build.format 'preserve', @astrojs/sitemap does not append a
// trailing slash to directory-index routes. Normalize so canonical URLs
Expand Down
1 change: 1 addition & 0 deletions scripts/verify-build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const REQUIRED_PAGES = [
'downloads/server/index.html',
'install/index.html',
'faq/index.html',
'compatibility/index.html',
'donate/index.html',
// Not linked from anywhere, but Creem validates this URL -- so a build
// that stops emitting it should fail rather than pass quietly.
Expand Down
110 changes: 110 additions & 0 deletions src/components/MacOSPicker.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
import { MACOS_RELEASES } from '@data/pricing';

/*
* A macOS release picker that highlights which features a given release can
* run. Shared by the pricing and compatibility pages so the two cannot drift.
*
* It operates on any `[data-feature][data-macos]` elements on the page: it sets
* data-state="included"/"excluded" on each and fills its `[data-verdict]` line.
* Styling lives with the cards, not here.
*
* Releases are cumulative -- a release runs everything introduced at or below
* its version -- which is a `<=` on the major version and nothing more.
*/
interface Props {
/** Text before the select. */
label: string;
/** Noun for the count line, e.g. "Pro features" or "features". */
noun: string;
}
const { label, noun } = Astro.props;

// Default to the newest release. Derived from the data, so adding macOS 27
// moves the default automatically.
const newest = MACOS_RELEASES.reduce((a, b) => (b.version > a.version ? b : a));
---

<div class="flex flex-wrap items-center gap-3" data-macos-picker data-noun={noun}>
<label for="macos-filter" class="text-sm font-medium text-strong">{label}</label>

{/*
appearance-none plus our own chevron: the native arrow sits inside the right
padding, and a rounded-full pill's corner radius curves in over it.
*/}
<div class="relative inline-flex">
<select
id="macos-filter"
class="appearance-none rounded-full border border-line bg-page py-2 pl-4 pr-10
text-sm font-medium text-strong
focus-visible:border-accent focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-accent"
>
{
MACOS_RELEASES.map((release) => (
<option value={release.version} selected={release.version === newest.version}>
{release.name} ({release.version})
</option>
))
}
</select>
<svg
class="pointer-events-none absolute right-3.5 top-1/2 size-4 -translate-y-1/2 text-muted"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<path d="M5 7.5 10 12.5 15 7.5" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</div>

<p id="macos-summary" role="status" aria-live="polite" class="text-sm text-muted">
<slot name="summary" />
</p>
</div>

<script>
const picker = document.querySelector<HTMLElement>('[data-macos-picker]');
const select = document.getElementById('macos-filter') as HTMLSelectElement | null;
const summary = document.getElementById('macos-summary');
const cards = [...document.querySelectorAll<HTMLElement>('[data-feature]')];

if (picker && select && cards.length) {
const noun = picker.dataset.noun ?? 'features';
const names = new Map<number, string>(
[...select.options].map((option) => [Number(option.value), option.textContent?.trim() ?? ''])
);

const apply = () => {
const chosen = Number(select.value);
if (!chosen) return;

let included = 0;
for (const card of cards) {
const needs = Number(card.dataset.macos);
const ok = needs <= chosen;
if (ok) included += 1;
card.dataset.state = ok ? 'included' : 'excluded';

const verdict = card.querySelector<HTMLElement>('[data-verdict]');
if (verdict) {
verdict.textContent = ok ? '✓ Supported' : `Needs macOS ${needs}`;
verdict.className = ok
? 'mt-3 text-xs font-semibold text-accent'
: 'mt-3 text-xs font-semibold text-muted';
}
}

if (summary) {
summary.textContent =
`${included} of ${cards.length} ${noun} run on ${names.get(chosen) ?? chosen}.`;
}
};

select.addEventListener('change', apply);
// Browsers restore a previous selection on reload; reflect it immediately.
apply();
}
</script>
223 changes: 223 additions & 0 deletions src/data/capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/** The Private API capability catalogue, for the compatibility page.
*
* Copied from the server's `Sources/BBPrivateAPICatalog/CapabilityCatalog.swift`
* -- id, title, summary, macOS floor and category all come from there, so the
* page and the server describe the same thing. If a capability is added or its
* macOS floor moves, update it here too.
*
* Deliberately no subscription tier: this page answers "does my Mac support
* it", which is a separate question from what a plan includes, and Pro is not
* announced yet. Adding a tier badge later is additive.
*/

export interface Capability {
id: string;
title: string;
summary: string;
/** Minimum macOS major version on the server Mac. */
macos: number;
category: CapabilityCategory;
}

export type CapabilityCategory =
| 'messages'
| 'reactions'
| 'organising'
| 'faceTime'
| 'findMy';

/** Section headings, in the order they appear on the page. */
export const CAPABILITY_CATEGORIES: readonly { id: CapabilityCategory; label: string }[] = [
{ id: 'messages', label: 'Messaging' },
{ id: 'reactions', label: 'Reactions and stickers' },
{ id: 'organising', label: 'Organising conversations' },
{ id: 'faceTime', label: 'FaceTime' },
{ id: 'findMy', label: 'Find My' },
] as const;

export const CAPABILITIES: readonly Capability[] = [
{
id: 'rich-sending',
title: 'Improved message sending',
summary:
'Send with a subject line, and more reliably than the scripting interface manages.',
macos: 14,
category: 'messages',
},
{
id: 'message-effects',
title: 'Bubble and screen effects',
summary:
'Send with slam, loud, gentle, invisible ink, confetti, fireworks and the rest.',
macos: 14,
category: 'messages',
},
{
id: 'replies',
title: 'Threaded replies',
summary:
'Reply to one specific message, so the conversation keeps the thread.',
macos: 14,
category: 'messages',
},
{
id: 'mentions',
title: 'Mentions',
summary:
'Mention someone by name in a group so they are notified even on Do Not Disturb.',
macos: 14,
category: 'messages',
},
{
id: 'edit-message',
title: 'Edit a sent message',
summary:
'Change the wording of a message you already sent, as Messages does.',
macos: 14,
category: 'messages',
},
{
id: 'unsend-message',
title: 'Unsend a message',
summary:
'Take back a message so it disappears for everyone in the conversation.',
macos: 14,
category: 'messages',
},
{
id: 'typing-indicators',
title: 'Typing indicators',
summary:
'See when someone is typing to you, and show them when you are.',
macos: 14,
category: 'messages',
},
{
id: 'read-state',
title: 'Mark as read or unread',
summary:
'Clear a conversation\'s badge, or put it back, from a client.',
macos: 14,
category: 'messages',
},
{
id: 'send-later',
title: 'Send Later',
summary:
'Hand a message to iMessage to deliver later. Apple sends it whether or not this Mac is running, unlike the server\'s own scheduled messages.',
macos: 15,
category: 'messages',
},
{
id: 'text-formatting',
title: 'Text formatting',
summary:
'Bold, italics, underline and strikethrough inside a message.',
macos: 15,
category: 'messages',
},
{
id: 'polls',
title: 'Polls',
summary:
'Create a poll in a group and collect votes from everyone in it.',
macos: 26,
category: 'messages',
},
{
id: 'tapbacks',
title: 'Reactions',
summary:
'Love, like, dislike, laugh, emphasise and question, on any message.',
macos: 14,
category: 'reactions',
},
{
id: 'stickers',
title: 'Stickers',
summary:
'Send a sticker, and place it on a message the way Messages does.',
macos: 14,
category: 'reactions',
},
{
id: 'emoji-reactions',
title: 'Emoji reactions',
summary:
'React with any emoji, not just the six built-in tapbacks.',
macos: 15,
category: 'reactions',
},
{
id: 'sticker-reactions',
title: 'Sticker reactions',
summary:
'Send a sticker as a reaction to a message, the way a tapback attaches to it.',
macos: 15,
category: 'reactions',
},
{
id: 'group-management',
title: 'Managing groups',
summary:
'Rename a group, add or remove people, change its photo, and leave it.',
macos: 14,
category: 'organising',
},
{
id: 'pinning',
title: 'Pinning conversations',
summary:
'Pin a conversation to the top, in the same order as your other devices.',
macos: 14,
category: 'organising',
},
{
id: 'muting',
title: 'Muting conversations',
summary:
'Silence a conversation, permanently or until a time you choose.',
macos: 14,
category: 'organising',
},
{
id: 'junk-reporting',
title: 'Spam and junk',
summary:
'Report a conversation as junk, mark it as spam, and move it out of Unknown Senders.',
macos: 14,
category: 'organising',
},
{
id: 'chat-backgrounds',
title: 'Conversation backgrounds',
summary:
'Set a background image for a conversation, shared with everyone in it.',
macos: 26,
category: 'organising',
},
{
id: 'screen-unknown-senders',
title: 'Screen unknown senders',
summary:
'See whether a sender is known, and accept one into your contacts.',
macos: 26,
category: 'organising',
},
{
id: 'facetime',
title: 'FaceTime from a client',
summary:
'Answer or end a call, and create a link someone can join from anywhere.',
macos: 14,
category: 'faceTime',
},
{
id: 'find-my',
title: 'Find My',
summary:
'See where your friends and devices are.',
macos: 14,
category: 'findMy',
},
] as const;
Loading
Loading