feat(skills): add 5 Orbit briefing templates (#671)

* feat(skills): add 5 Orbit briefing templates

Introduces a new "orbit" scenario family in the Examples gallery for
morning-briefing surfaces. Each template lives at the top of "我的设计"
and aggregates yesterday's connector activity into a single page.

- orbit-general: adaptive bento dashboard that fans across 12-16
  connectors, where each module picks its own UI form by data type
  (list / avatar stack / status ring / heatmap / file grid / alert
  card / kanban / etc.)
- orbit-github: GitHub-flavored single-connector digest mirroring the
  Notifications + PR-diff visual language
- orbit-gmail: Gmail-flavored digest rendered as a Daily Digest email
  inside the three-pane inbox
- orbit-linear: Linear-flavored digest in the dark Inbox + cycle-
  progress layout
- orbit-notion: Notion-flavored digest authored as a native Notion
  page (callout / toggle / database table)

The new scenario value 'orbit' surfaces as a filter pill in
ExamplesTab automatically; no UI code change required.

* fix(skills): reframe Orbit skill descriptions as pipeline-triggered

The original descriptions framed each skill as a standalone "X-flavored
briefing template" the user picks. They are actually skills the Orbit
daily-digest pipeline selects automatically based on which connectors
the user has authenticated, then runs against live connector data.

Rewrites both `description` and `example_prompt` for all 5 templates:
- orbit-general: invoked when 2+ connectors are connected; aggregates
  the past 24h across every authenticated source
- orbit-github / orbit-gmail / orbit-linear / orbit-notion: invoked
  when the named connector is the user's only connection (or scope is
  explicitly limited to it); pulls the past 24h from that connection
  alone

All 5 now state explicitly that they are not user-triggered — the
Orbit scheduler invokes them.

* feat(examples): add Orbit pill to the mode filter row

Surfaces the Orbit briefing skills as a top-level "type" filter in the
Examples gallery, alongside Prototype · Desktop / Mobile / Slide deck /
Docs & templates. Filter matches skills with scenario === 'orbit'.

- ExamplesTab: extend ModeFilter and MODE_PILLS with 'orbit'; teach
  matchesMode and modeCounts about it
- i18n: add 'examples.modeOrbit' to Dict and to all 16 locale files
  ('Orbit' is left untranslated as a brand name)

* polish(orbit-general): real Figma preview image + revised comment

Replaces the empty gray placeholder in the Figma module with an
Unsplash UI-design photo, and rewrites the mock comment to read like
a substantive design-review note rather than a nit about button
placement.

* feat(examples): eager-load card previews via IntersectionObserver

Card previews previously only loaded on hover, leaving the example
gallery showing 'Hover to preview' placeholders for everything below
the fold. Now each card observes the viewport and prefetches its HTML
800px before scrolling into view, so the iframe is ready by the time
the user reaches it.

Hover remains as a fallback path (and for browsers without
IntersectionObserver, the card loads immediately on mount).

Also reverts the Unsplash photo on the orbit-general Figma module
back to the gray placeholder — the stock image semantically misread
as a Photoshop screenshot rather than a Figma artboard.

* feat(orbit-general): drop Figma connector module

Removes the Figma bento card and its scoped CSS, plus the orphaned
Top-3 entry that referenced a Figma comment. Reassigns Top-3 #2 to
a Notion document review so the priority list stays aligned with
the connectors actually rendered.

* i18n(skills): translate Orbit example prompts to English

The example_prompt is what gets injected into the chat input when a
user clicks 'Use this prompt', and is read by the agent verbatim. It
should match the SKILL.md description language (English), not the UI
locale. Replaces the Chinese drafts with English equivalents across
all 5 Orbit skills, and drops the Figma reference from orbit-general
since that connector module was removed earlier.

* fix(skills): rewrite Orbit SKILL.md bodies with reproducible specs

Earlier the bodies were too abstract (only a connector→UI mapping
table and a one-line style note), so agents running the skill could
not reproduce the shipped example.html and got stuck in long retries.

Each SKILL.md body now contains:
- exact color tokens lifted from the example.html
- type stack and font sizes
- a section-by-section page spec (top-to-bottom)
- chip / pill / icon rendering rules
- forbidden list

The example_prompt is collapsed back to a one-line user intent so the
skill body is the source of design truth.

Covers all 5 templates: orbit-general, orbit-github, orbit-gmail,
orbit-linear, orbit-notion.

* feat(orbit): make every connector item clickable

Each Orbit briefing template now links its rows / cards to the matching
source URL so users can jump straight from the morning digest to the
underlying connector.

- orbit-general: each bento card gains an 'Open in {connector} ↗' CTA
  built from a connector→URL map; each Top 3 card becomes an anchor
- orbit-github: every event row opens the corresponding github.com
  pull/issue URL parsed from the row identifier; the header logo links
  to the repo
- orbit-linear: each issue row gains a small ↗ button that opens
  linear.app/{team}/issue/{ID}
- orbit-gmail: action and reply buttons jump to a Gmail search URL
  scoped to the sender
- orbit-notion: page-link spans wrap as anchors and database rows are
  click-to-open against notion.so

All links use target="_blank" rel="noopener noreferrer".

* fix(skills): force agents to mirror example.html 1:1

Earlier skills told the agent the example was 'source of truth' but
left phrasing soft, so agents felt free to add extra UI elements
(snoozed-mail row, extra yellow stars on inbox rows, etc.) that
were not in example.html.

Each Orbit SKILL.md now opens with a 'Source-of-truth protocol' that
forces the agent to:
  1. read example.html before writing any output
  2. mirror its DOM structure / class names / module count / element
     order 1:1
  3. only refresh mock values; never invent additional UI elements,
     rail entries, sections, badges, or chrome ornaments

The reference sections that follow stay informative for tokens and
visual language but are explicitly demoted from spec to commentary.

* fix(orbit-gmail): remove three-pane / left-rail / inbox-list claims

The example.html is a single-column page: Gmail top header + the
opened Orbit Daily Digest email (toolbar / subject / sender / digest
body / reply bar). Earlier copy described a Gmail three-pane app with
Compose button, label list, Categories tabs, and an inbox listing —
none of which exist in the actual asset.

- example_prompt: drops 'three-pane inbox' phrasing
- description: same
- body: rewrites Page sections to mirror the real header → email-chrome
  layout, top to bottom; explicitly forbids left rail, inbox list, and
  Categories tab strip

* feat(orbit): forbid external design systems in all 5 skills

Each Orbit briefing template ships its own complete visual language
baked into example.html (Gmail / GitHub / Linear / Notion / Open
Orbit's editorial bento). Adds an explicit 'Design system policy'
block telling the agent to:

- ignore any DESIGN.md attached to the active project
- ignore brand tokens or Figma files supplied via chat
- use exclusively the colors / fonts / radii from example.html

This is a hard constraint: an Orbit briefing must look like the
connector it represents, not like the user's brand.

* feat(newproj): hide design-system picker for skills that opt out

Skills can declare 'od.design_system.requires: false' in SKILL.md to
opt out of DESIGN.md injection (the Orbit briefing skills do this —
their example.html ships with a complete connector-native visual
language). When the active default skill for a tab opts out, hide the
design-system picker so we don't ask the user to attach a brand we'll
then ignore.

Existing tabs that don't host a default skill (template, other) keep
the picker. The check only fires for prototype / live-artifact / deck.

* review: address P2 reviewer feedback

P2 — Connector family coverage gaps (orbit-general):
  Adds Finance, CRM/Sales, Support, Analytics, Infrastructure, Security
  rows to the connector→UI mapping table (now 16 families). Adds a
  'Fallback heuristics' subsection so unknown connectors are routed by
  data shape (numbers + time series → Alerts, rows + status field →
  Task mgmt, etc.).

P2 — 'Forbidden' rules too vague (all 5 skills):
  Rewrites every Forbidden section as a paired 'Don't / Do' constraints
  table so each negative is paired with a concrete positive. Replaces
  obvious bans (lorem ipsum) with substantive ones (real-shaped mock
  copy, plausible identifiers, dev-team label hues, etc.).

* ci: register orbit skills in de/ru/fr en-fallback lists

The localized-content coverage test asserts that every skill in
skills/ is either translated or explicitly declared as falling back
to English in the LOCALIZED_CONTENT_IDS bundle. The 5 new orbit
skills weren't in any bundle, so the workspace validation job failed
on the de/ru/fr coverage assertions.

Adds the 5 orbit-* ids to DE/FR/RU_SKILL_IDS_WITH_EN_FALLBACK so
those locales explicitly fall back to the SKILL.md English copy
(matching the minimal-change posture chosen earlier in this PR).
This commit is contained in:
Eli 2026-05-06 21:39:52 +08:00 committed by GitHub
parent 80416b185a
commit 3298cb3756
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 4782 additions and 3 deletions

View file

@ -19,7 +19,7 @@ interface Props {
onUsePrompt: (skill: SkillSummary) => void;
}
type ModeFilter = 'all' | 'prototype-desktop' | 'prototype-mobile' | 'deck' | 'document';
type ModeFilter = 'all' | 'prototype-desktop' | 'prototype-mobile' | 'deck' | 'document' | 'orbit';
type SurfaceFilter = 'all' | Surface;
type ScenarioFilter = string;
@ -37,6 +37,7 @@ const MODE_PILLS: { value: ModeFilter; labelKey: keyof Dict }[] = [
{ value: 'prototype-mobile', labelKey: 'examples.modePrototypeMobile' },
{ value: 'deck', labelKey: 'examples.modeDeck' },
{ value: 'document', labelKey: 'examples.modeDocument' },
{ value: 'orbit', labelKey: 'examples.modeOrbit' },
];
const SCENARIO_LABEL_KEY: Record<string, keyof Dict> = {
@ -85,6 +86,7 @@ function matchesMode(skill: SkillSummary, filter: ModeFilter): boolean {
if (filter === 'prototype-mobile')
return skill.mode === 'prototype' && skill.platform === 'mobile';
if (filter === 'document') return skill.mode === 'template';
if (filter === 'orbit') return skill.scenario === 'orbit';
return true;
}
@ -147,12 +149,14 @@ export function ExamplesTab({ skills, onUsePrompt }: Props) {
'prototype-mobile': 0,
deck: 0,
document: 0,
orbit: 0,
};
for (const s of surfaceScoped) {
if (matchesMode(s, 'prototype-desktop')) c['prototype-desktop']++;
if (matchesMode(s, 'prototype-mobile')) c['prototype-mobile']++;
if (matchesMode(s, 'deck')) c.deck++;
if (matchesMode(s, 'document')) c.document++;
if (matchesMode(s, 'orbit')) c.orbit++;
}
return c;
}, [skills, surfaceFilter]);
@ -357,7 +361,41 @@ function ExampleCard({
const { locale, t } = useI18n();
const [hovered, setHovered] = useState(false);
const [shareOpen, setShareOpen] = useState(false);
const [intersected, setIntersected] = useState(false);
const shareRef = useRef<HTMLDivElement | null>(null);
const cardRef = useRef<HTMLDivElement | null>(null);
// Eagerly request the preview HTML once the card scrolls near the viewport.
// The 800px bottom rootMargin prefetches cards that are about to be
// scrolled into view so the iframe is ready by the time the user reaches
// it. Hover (below) is kept as a fallback for environments that lack
// IntersectionObserver or for cards already visible on first paint that
// somehow miss the initial observation.
useEffect(() => {
if (intersected) return;
const node = cardRef.current;
if (!node) return;
if (typeof IntersectionObserver === 'undefined') {
setIntersected(true);
onLoad();
return;
}
const obs = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
setIntersected(true);
onLoad();
obs.disconnect();
break;
}
}
},
{ rootMargin: '0px 0px 800px 0px' },
);
obs.observe(node);
return () => obs.disconnect();
}, [intersected, onLoad]);
useEffect(() => {
if (!shareOpen) return;
@ -384,6 +422,7 @@ function ExampleCard({
return (
<div
ref={cardRef}
className="example-card"
data-testid={`example-card-${skill.id}`}
onMouseEnter={() => {
@ -419,7 +458,7 @@ function ExampleCard({
</>
) : (
<div className="example-preview-placeholder">
{hovered
{hovered || intersected
? t('examples.loadingPreview')
: t('examples.hoverPreview')}
</div>

View file

@ -135,11 +135,36 @@ export function NewProjectPanel({
// media surfaces use prompt templates instead — design tokens don't map
// onto image/video/audio generations, and the picker just adds noise
// there. Keep this list explicit so future tabs declare their intent.
const showDesignSystemPicker =
const tabSupportsDesignSystem =
tab === 'prototype' ||
tab === 'deck' ||
tab === 'template' ||
tab === 'other';
// Some skills (e.g. the Orbit briefings) ship their own complete visual
// language baked into example.html and explicitly opt out of DESIGN.md
// injection via `od.design_system.requires: false`. When such a skill is
// the active default for the current tab, hide the picker entirely so
// the user isn't asked to attach a brand we'll then ignore.
const tabDefaultSkillForcesNoDs = useMemo(() => {
const tabSkillId = ((): string | null => {
if (tab === 'prototype' || tab === 'live-artifact') {
const list = skills.filter((s) => s.mode === 'prototype');
return list.find((s) => s.defaultFor.includes('prototype'))?.id
?? list[0]?.id ?? null;
}
if (tab === 'deck') {
const list = skills.filter((s) => s.mode === 'deck');
return list.find((s) => s.defaultFor.includes('deck'))?.id
?? list[0]?.id ?? null;
}
return null;
})();
if (!tabSkillId) return false;
const s = skills.find((x) => x.id === tabSkillId);
return s ? s.designSystemRequired === false : false;
}, [tab, skills]);
const showDesignSystemPicker =
tabSupportsDesignSystem && !tabDefaultSkillForcesNoDs;
// When entering the template tab, snap to the first user-saved template
// if there is one (and we don't already have a valid pick). The template

View file

@ -315,6 +315,11 @@ export const FR_DESIGN_SYSTEM_CATEGORIES: Record<string, string> = {
export const FR_SKILL_IDS_WITH_EN_FALLBACK = [
'html-ppt-taste-brutalist',
'html-ppt-taste-editorial',
'orbit-general',
'orbit-github',
'orbit-gmail',
'orbit-linear',
'orbit-notion',
'web-prototype-taste-brutalist',
'web-prototype-taste-editorial',
'web-prototype-taste-soft',

View file

@ -315,6 +315,11 @@ export const RU_DESIGN_SYSTEM_CATEGORIES: Record<string, string> = {
export const RU_SKILL_IDS_WITH_EN_FALLBACK = [
'html-ppt-taste-brutalist',
'html-ppt-taste-editorial',
'orbit-general',
'orbit-github',
'orbit-gmail',
'orbit-linear',
'orbit-notion',
'web-prototype-taste-brutalist',
'web-prototype-taste-editorial',
'web-prototype-taste-soft',

View file

@ -364,6 +364,11 @@ const DE_DESIGN_SYSTEM_CATEGORIES: Record<string, string> = {
const DE_SKILL_IDS_WITH_EN_FALLBACK = [
'html-ppt-taste-brutalist',
'html-ppt-taste-editorial',
'orbit-general',
'orbit-github',
'orbit-gmail',
'orbit-linear',
'orbit-notion',
'web-prototype-taste-brutalist',
'web-prototype-taste-editorial',
'web-prototype-taste-soft',

View file

@ -325,6 +325,7 @@ export const ar: Dict = {
'examples.modePrototypeMobile': 'نماذج أولية · الجوال',
'examples.modeDeck': 'شرائح',
'examples.modeDocument': 'مستندات وقوالب',
'examples.modeOrbit': 'Orbit',
'examples.scenarioGeneral': 'عام',
'examples.scenarioEngineering': 'هندسة',
'examples.scenarioProduct': 'منتج',

View file

@ -281,6 +281,7 @@ export const de: Dict = {
'examples.modePrototypeMobile': 'Prototypen · Mobil',
'examples.modeDeck': 'Folien',
'examples.modeDocument': 'Dokumente & Templates',
'examples.modeOrbit': 'Orbit',
'examples.scenarioGeneral': 'Allgemein',
'examples.scenarioEngineering': 'Engineering',
'examples.scenarioProduct': 'Produkt',

View file

@ -338,6 +338,7 @@ export const en: Dict = {
'examples.modePrototypeMobile': 'Prototypes · Mobile',
'examples.modeDeck': 'Slides',
'examples.modeDocument': 'Docs & templates',
'examples.modeOrbit': 'Orbit',
'examples.scenarioGeneral': 'General',
'examples.scenarioEngineering': 'Engineering',
'examples.scenarioProduct': 'Product',

View file

@ -282,6 +282,7 @@ export const esES: Dict = {
'examples.modePrototypeMobile': 'Prototipos · Móvil',
'examples.modeDeck': 'Diapositivas',
'examples.modeDocument': 'Documentos y plantillas',
'examples.modeOrbit': 'Orbit',
'examples.scenarioGeneral': 'General',
'examples.scenarioEngineering': 'Ingeniería',
'examples.scenarioProduct': 'Producto',

View file

@ -338,6 +338,7 @@ export const fa: Dict = {
'examples.modePrototypeMobile': 'نمونه اولیه · موبایل',
'examples.modeDeck': 'اسلایدها',
'examples.modeDocument': 'اسناد و قالب‌ها',
'examples.modeOrbit': 'Orbit',
'examples.scenarioGeneral': 'عمومی',
'examples.scenarioEngineering': 'مهندسی',
'examples.scenarioProduct': 'محصول',

View file

@ -325,6 +325,7 @@ export const fr: Dict = {
'examples.modePrototypeMobile': 'Prototypes · Mobile',
'examples.modeDeck': 'Diaporamas',
'examples.modeDocument': 'Docs et modèles',
'examples.modeOrbit': 'Orbit',
'examples.scenarioGeneral': 'Général',
'examples.scenarioEngineering': 'Ingénierie',
'examples.scenarioProduct': 'Produit',

View file

@ -327,6 +327,7 @@ export const hu: Dict = {
'examples.modePrototypeMobile': 'Prototípusok · Mobil',
'examples.modeDeck': 'Diák',
'examples.modeDocument': 'Dokumentumok és sablonok',
'examples.modeOrbit': 'Orbit',
'examples.scenarioGeneral': 'Általános',
'examples.scenarioEngineering': 'Mérnöki',
'examples.scenarioProduct': 'Termék',

View file

@ -280,6 +280,7 @@ export const ja: Dict = {
'examples.modePrototypeMobile': 'プロトタイプ · モバイル',
'examples.modeDeck': 'スライド',
'examples.modeDocument': 'ドキュメント & テンプレート',
'examples.modeOrbit': 'Orbit',
'examples.scenarioGeneral': '一般',
'examples.scenarioEngineering': 'エンジニアリング',
'examples.scenarioProduct': 'プロダクト',

View file

@ -327,6 +327,7 @@ export const ko: Dict = {
'examples.modePrototypeMobile': '프로토타입 · 모바일',
'examples.modeDeck': '슬라이드',
'examples.modeDocument': '문서 및 템플릿',
'examples.modeOrbit': 'Orbit',
'examples.scenarioGeneral': '일반',
'examples.scenarioEngineering': '엔지니어링',
'examples.scenarioProduct': '제품',

View file

@ -327,6 +327,7 @@ export const pl: Dict = {
'examples.modePrototypeMobile': 'Prototypy · Mobile',
'examples.modeDeck': 'Slajdy',
'examples.modeDocument': 'Dokumenty i szablony',
'examples.modeOrbit': 'Orbit',
'examples.scenarioGeneral': 'Ogólne',
'examples.scenarioEngineering': 'Inżynieria',
'examples.scenarioProduct': 'Produkt',

View file

@ -337,6 +337,7 @@ export const ptBR: Dict = {
'examples.modePrototypeMobile': 'Protótipos · Mobile',
'examples.modeDeck': 'Slides',
'examples.modeDocument': 'Docs e templates',
'examples.modeOrbit': 'Orbit',
'examples.scenarioGeneral': 'Geral',
'examples.scenarioEngineering': 'Engenharia',
'examples.scenarioProduct': 'Produto',

View file

@ -337,6 +337,7 @@ export const ru: Dict = {
'examples.modePrototypeMobile': 'Прототипы · Мобильные',
'examples.modeDeck': 'Презентации',
'examples.modeDocument': 'Документы и шаблоны',
'examples.modeOrbit': 'Orbit',
'examples.scenarioGeneral': 'Общее',
'examples.scenarioEngineering': 'Инженерия',
'examples.scenarioProduct': 'Продукт',

View file

@ -321,6 +321,7 @@ export const tr: Dict = {
'examples.modePrototypeMobile': 'Prototipler · Mobil',
'examples.modeDeck': 'Slaytlar',
'examples.modeDocument': 'Doküman & şablonlar',
'examples.modeOrbit': 'Orbit',
'examples.scenarioGeneral': 'Genel',
'examples.scenarioEngineering': 'Mühendislik',
'examples.scenarioProduct': 'Ürün',

View file

@ -338,6 +338,7 @@ export const uk: Dict = {
'examples.modePrototypeMobile': 'Прототипи · Мобільний',
'examples.modeDeck': 'Слайди',
'examples.modeDocument': 'Документи та шаблони',
'examples.modeOrbit': 'Orbit',
'examples.scenarioGeneral': 'Загальне',
'examples.scenarioEngineering': 'Інженерія',
'examples.scenarioProduct': 'Продукт',

View file

@ -332,6 +332,7 @@ export const zhCN: Dict = {
'examples.modePrototypeMobile': '原型 · 移动端',
'examples.modeDeck': '幻灯片',
'examples.modeDocument': '文档与模板',
'examples.modeOrbit': 'Orbit',
'examples.scenarioGeneral': '通用',
'examples.scenarioEngineering': '工程',
'examples.scenarioProduct': '产品',

View file

@ -332,6 +332,7 @@ export const zhTW: Dict = {
'examples.modePrototypeMobile': '原型 · 行動版',
'examples.modeDeck': '投影片',
'examples.modeDocument': '文件與範本',
'examples.modeOrbit': 'Orbit',
'examples.scenarioGeneral': '通用',
'examples.scenarioEngineering': '工程',
'examples.scenarioProduct': '產品',

View file

@ -398,6 +398,7 @@ export interface Dict {
'examples.modePrototypeMobile': string;
'examples.modeDeck': string;
'examples.modeDocument': string;
'examples.modeOrbit': string;
'examples.scenarioGeneral': string;
'examples.scenarioEngineering': string;
'examples.scenarioProduct': string;

View file

@ -0,0 +1,196 @@
---
name: orbit-general
description: |
Open Orbit briefing skill — selected by the Orbit pipeline when the
user has two or more connectors connected. Pulls the past 24 hours of
activity from every authenticated connector (GitHub, Linear, Notion,
Slack, 飞书, Calendar, Gmail, Drive, Sentry, Vercel, …) and renders a
single adaptive bento-grid dashboard at the top of "我的设计". Each
connector module picks its own UI form (list, avatar stack, status
ring, heatmap, file grid, alert card, …) based on the data shape it
returns, so the layout scales as Orbit's connector ecosystem grows.
This skill should not be triggered manually — it is invoked by
Orbit's daily-digest scheduler against the user's live connector
data.
triggers:
- "orbit"
- "daily digest"
- "morning briefing"
- "早安简报"
- "每日简报"
- "跨工具汇总"
od:
mode: prototype
platform: desktop
scenario: orbit
featured: 1
preview:
type: html
entry: index.html
design_system:
requires: false
example_prompt: "Generate today's Open Orbit morning briefing. I have ~10 connectors connected (GitHub, Linear, Notion, Calendar, 飞书, Sentry, Vercel, Slack, Gmail, Drive). Pull yesterday's activity from each and render the editorial bento dashboard."
---
# Orbit General Briefing
Cross-connector morning briefing that lives at the top of "我的设计".
Pulls the past 24 hours of activity from every authenticated connector
and lays them out as one editorial bento dashboard.
## ⚠️ Source-of-truth protocol (read this first)
**Step 1.** Open and read the shipped `example.html` in this folder
before writing any output. That file is the canonical design — your
job is to **reproduce it**, not reinterpret it.
**Step 2.** Mirror the example's structure 1:1:
- Same DOM hierarchy and class names
- Same number and order of sections
- Same number of bento modules in the same order
- Same connector list (do **not** add or drop connectors)
- Same KPI labels, same Top 3 entries, same "people waiting" set
- Same footer string
- Same `<script>` block at the end (link injection)
**Step 3.** You may freshen mock data values (counts, names, times) so
they read as "today" — but you must not invent new UI elements,
sections, modules, badges, callouts, ribbons, banners, decorations or
chrome that aren't already in `example.html`. If a detail is not in
the example, it does not belong in your output.
The body sections below are a **reference for the visual language and
tokens** — they are not a license to add features the example doesn't
already render.
## ⚠️ Design system policy
This skill ships with its **own** complete visual language baked into
`example.html`. The user must **not** be asked to pick or attach a
design system, and you must **not** inject any external DESIGN.md
tokens into the output.
- If the active project has a design system attached, **ignore it**.
- If the user supplies brand tokens or a Figma file, **ignore them**.
- Use exclusively the colors / fonts / radii / chrome defined in
`example.html`.
This is a hard constraint: an Orbit briefing must read as Open Orbit's
own editorial bento language, not as the user's brand.
## Canvas tokens (use these exact values)
```
--bg: #FAF7F2 /* off-white page */
--surface: #FFFFFF /* card */
--fg: #1A1816 /* ink */
--muted: #6B6660 /* secondary text */
--border: #EAE5DD /* 1px hairline only */
--orange: #D86A47 /* accent (CTAs, hero highlight, meeting blocks) */
--green: #2E7D5B /* ok / done */
--yellow: #C9982E /* waiting */
--red: #C0473A /* alert / fail */
--radius-l: 24px /* outer container */
--radius-m: 16px /* bento cards */
--radius-s: 12px /* inner blocks */
```
Type stack:
- Display serif: `'Cormorant', Georgia, serif` — KPI numerals, Hero h1,
Top 3 serial numbers, italic comment quotes
- Body sans: `'Inter', -apple-system, system-ui, sans-serif`
- Numbers: always `font-variant-numeric: tabular-nums`
No shadows. No gradients. No emoji as primary visuals.
Connector icons must be monochrome line SVG (1.5 stroke).
## Page sections (top to bottom)
1. **Hero** — single row, ~80px tall.
Left: `☀ 早安, Eli` (Cormorant 38px, with `,` in `--orange`).
Right of name: `· 2026 年 5 月 6 日 · 星期三` (muted, 18px).
Far right: round avatar (40px) + small ⚙ + ✕ icons.
2. **KPI strip** — single row, ~120px tall, 5 columns equal width.
Each cell: serif number (Cormorant 64px, `--fg`) over a muted
uppercase tracking label (Inter 11px, letter-spacing 0.06em).
Optional ▲/▼ delta tag in `--green`/`--red` next to the number.
Suggested labels: `待办 / 待 review / 会议 / @ 我 / agent 跑完`.
3. **Today's timeline** — full width, ~140px tall.
Horizontal time axis from 09:00 → 19:00, hour ticks below.
Meeting blocks: filled `--orange` rounded rectangles spanning their
start/end, with the meeting name + attendee count inside.
Deep-work suggestions: pale-green translucent bands behind the axis.
"Now" indicator: a 1px vertical `--red` line with a pulsing dot
(`@keyframes pulse 2s ease-in-out infinite`) and a tiny `现在` label.
4. **Top 3** — 3 equal cards, ~220px tall.
Each card: huge serif numeral 1 / 2 / 3 (Cormorant 96px, in `--fg`)
left-aligned; one-sentence task headline (Inter 18px medium); a
meta row at the bottom with the connector source label + line-icon
+ `等待 Xh` waiting time. Cards have `--border` 1px outline only.
5. **Connector modules** — adaptive bento, the heart of the briefing.
Render 1016 modules. Sizes vary: data-rich connectors take a
2-column or 2-row span, simple ones stay 1×1. **No two modules
should look identical.** Pick UI per the data family below.
6. **People waiting on you** — full-width strip ~110px tall.
Title left: `5 人在等你 · 最久 22h` (serif 24px).
Right: 5 overlapping circular avatars (44px, ~8px overlap), each
with the person's name + waiting reason underneath in 12px muted.
7. **Footer** — single line, ~52px.
Left: `Open Orbit · auto-generated 06:42 · N connectors`.
Right: `由 Nexu Labs 出品`.
Border-top 1px, all text 12px muted.
## Connector → UI mapping (pick the matching family)
| Family | Examples | UI form |
|---------------|---------------------------------------|------------------------------------------------------|
| Code collab | GitHub, GitLab, Bitbucket | Status-dot list (open/merged/closed/CI fail) + reviewer count, optional 23 line diff preview |
| Task mgmt | Linear, Jira, Asana, ClickUp | Issue list with colored status dot + priority bars; for cycle, add a small ring or progress strip |
| Comms | Gmail, Slack, 飞书 IM, Outlook | Round avatar + one-line quote, accent color for "awaiting reply" |
| Knowledge | Notion, Confluence, 飞书 Doc | Doc title + 2-line excerpt block; comment quote in italic serif |
| Time | Calendar | Already lives in the global timeline; module form: agenda list with start time gutter |
| Alerts | Sentry, Datadog, PagerDuty | Big red Cormorant number (e.g. `4`), 7 small squares as 7-day heatmap, plus 1 latest error line |
| Status | Vercel, GH Actions, Netlify | Colored status dot per recent build/deploy + branch + duration |
| Files | Drive, Dropbox, Box | Filename list with tiny thumbnail squares + "edited by" attribution |
| Board | Trello, Miro, FigJam | 3 compact kanban columns with rounded card chips |
| Finance | Stripe, PayPal, banking, Brex | Cormorant currency number + 7-day sparkline + last 3 transactions list |
| CRM / Sales | Salesforce, HubSpot, Pipedrive | 3-column deal pipeline (Open / Negotiation / Won) + 12 priority contact cards |
| Support | Zendesk, Intercom, Help Scout | Ticket queue list with SLA timer pill (green / yellow / red) + assignee avatar |
| Analytics | Google Analytics, Mixpanel, Amplitude | Mini funnel chart (4 bars descending) + 1-line cohort delta (`▲ 12% W/W`) |
| Infrastructure| AWS, GCP, Kubernetes, Docker | Resource meters (CPU / mem / disk percent bars) + last 2 deployment lines |
| Security | 1Password, Auth0, Okta | Event list with red shield for high-severity items + audit timestamp |
| Voice/Misc | unknown connector | See **Fallback heuristics** below |
### Fallback heuristics (for unknown connectors)
When a connector doesn't match any family above, infer by the **data
shape it returns**:
- Returns numbers + a time series → treat as **Alerts** (big number + heatmap)
- Returns rows with `status` field → treat as **Task mgmt** (status-dot list)
- Returns rows with `from` / `subject` → treat as **Comms** (avatar + quote)
- Returns documents / file names → treat as **Files** (list + thumbnails)
- Returns a small set of named "states" (deploy / build / cycle) → treat as **Status**
- Returns dated events → treat as **Time** (agenda list)
If still ambiguous, fall back to a status-dot list (the safest default).
## Implementation constraints (paired do / don't)
| Don't | Do |
|---|---|
| Render every module as the same card shape | Vary by family — Alert = big red number + heatmap; Status = status-dot list; Files = thumbnail grid; Comms = avatar + quote |
| Render Sentry / PagerDuty as a plain list | Big red Cormorant number + 7-day heatmap + latest error line (`TypeError: …`) |
| Render Calendar as a plain text agenda | Visualize on the horizontal timeline at the top; module form is an agenda list with start-time gutter |
| Use placeholder names like "Service A / Project X" | Infer plausible real names from the connector type — GitHub → `nexu-io/open-design`, Sentry → `frontend-prod`, Linear → `ENG / DES` cycle 24, Stripe → `Pro plan / Acme Co.` |
| Use lorem ipsum filler | Write specific mock copy that reads as a real workday — names, numbers, errors, paths, percentages |
| Mix emoji and SVG icons in the same module set | Use monochrome line SVGs (1.5 stroke) consistently for all connector icons; emoji are reserved for hero greeting and section anchors only |
| Square or rounded-square avatars | Always circles; sizes 28 / 32 / 40 / 44 px depending on context |
| Drop shadows / gradients / glows on cards | Flat surfaces only; differentiate cards with the 1px `#EAE5DD` hairline border |
| Use brand colors from the user's design system | Use exclusively the canvas tokens above (`#FAF7F2`, `#1A1816`, `#D86A47` …) — Orbit's own editorial language |

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,160 @@
---
name: orbit-github
description: |
Open Orbit briefing skill — selected by the Orbit pipeline when
GitHub is the user's only connected connector, or when the user
explicitly scopes their daily digest to GitHub. Pulls the past 24
hours of PRs, review requests, issues, CI runs, and merges from the
user's authenticated GitHub connection and renders them in a layout
that mirrors GitHub's native Notifications + PR-diff visual language.
This skill should not be triggered manually — it is invoked by
Orbit's daily-digest scheduler against live GitHub data.
triggers:
- "github briefing"
- "github digest"
- "pr digest"
- "github 简报"
- "代码活动汇总"
od:
mode: prototype
platform: desktop
scenario: orbit
featured: 2
preview:
type: html
entry: index.html
design_system:
requires: false
example_prompt: "Generate today's Open Orbit GitHub briefing. GitHub is my only connected connector — pull yesterday's PRs, review requests, issues, CI runs, and merges and render them as a GitHub Notifications + PR-diff page."
---
# Orbit · GitHub Briefing
Single-connector Orbit template scoped to GitHub.
## ⚠️ Source-of-truth protocol (read this first)
**Step 1.** Open and read the shipped `example.html` in this folder
before writing any output. That file is the canonical design — your
job is to **reproduce it**, not reinterpret it.
**Step 2.** Mirror the example's structure 1:1:
- Same DOM hierarchy and class names
- Same nav-bar items (and only those)
- Same left-rail filter list (and only those)
- Same event groups in the same order, with the same row count
- Same diff-preview placement, same CI-fail block, same attention block
- Same `<script>` block at the end (filter / hover / link injection)
**Step 3.** You may refresh mock values (PR numbers, titles, times,
CI commit messages) so they read as "today", but you must **not**
invent extra UI: no extra rail entries, no extra notifications,
no extra event types, no extra badges, no extra chrome ornaments. If
something is not already present in `example.html`, it does not
belong in your output.
The sections below are a **reference for tokens and visual language**
not a license to extend the page.
## ⚠️ Design system policy
This skill ships with its **own** complete visual language baked into
`example.html` (GitHub's Primer chrome). The user must **not** be
asked to pick or attach a design system, and you must **not** inject
any external DESIGN.md tokens into the output.
- If the active project has a design system attached, **ignore it**.
- If the user supplies brand tokens or a Figma file, **ignore them**.
- Use exclusively the colors / fonts / radii defined in `example.html`.
This is a hard constraint: the briefing must read as a real GitHub
page, not as the user's brand.
## Canvas tokens (use these exact values)
```
page bg: #f6f8fa
card bg: #ffffff
nav bar: #24292f /* GitHub black header */
nav text: #ffffff
ink: #1f2328
muted: #59636e
border: #d0d7de
hairline: rgba(208,215,222,0.32)
state · open: #1a7f37
state · merged: #8250df
state · closed: #cf222e
state · draft: #6e7781
attention bg: #fff8c5 /* yellow review-request block */
attention border: #d4a72c
ci-fail bg: #ffebe9
ci-fail border: #cf222e
```
Type stack:
- `-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif`
- Sizes: nav 14px, headings 16/20px, body 14px, meta 12px
## Page sections
1. **Top nav bar** — full-width, dark (`#24292f`), 60px tall.
Left: octocat SVG logo (white, 32px) + search input
(`rgba(255,255,255,0.08)` background, white placeholder ghosted).
Right: `+` plus dropdown, notifications bell with red dot if
unread > 0, round avatar.
2. **Header row** — light bar under the nav, 56px.
Left: page breadcrumb `Inbox · Daily Digest · May 6`.
Right: filter dropdown chips (`Type ▾ Date ▾ Status ▾`).
3. **Two-pane main**:
- **Left rail** (240px): vertical filter list. Items:
`Inbox · Saved · Done · All` then divider then
`Participating · Mentions · Review requests · Assigned · Comments`.
Active item: light gray pill background.
- **Main pane** (flex 1): event stream grouped by category.
4. **Category groups in main pane** (in this order):
- **Review requests waiting on you** — yellow attention block
(bg `#fff8c5`, 1px border `#d4a72c`). Each row: avatar + repo
path + PR title + reviewer-state row of small dots
(✓ green / ⏳ yellow / ○ gray) + "X of Y reviewers" + age.
- **CI / Checks** — each failed run is a red-bordered card
(border-color `#cf222e`, bg `#ffebe9`) with a `✗` red glyph,
run name, branch name (mono), commit message, age.
- **Issues assigned to you** — plain rows, status circle (open
green / closed red), title, repo path, age, label pills.
- **Activity** — quieter rows for merges/closes; muted text,
small `merged` purple pill or `closed` red pill.
5. **Optional PR-diff preview** — inline under one PR row, show
23 lines of mock code in a 12px monospace block with red `` /
green `+` prefixed lines and `#ffebe9` / `#dafbe1` row tints.
6. **Footer** — single line, 12px muted:
`Open Orbit · auto-generated 06:42 · GitHub only`.
## Pill / chip rules
- State pills: pill shape (border-radius 2em), 12px medium, 4×8 padding.
Foreground white, background by state color above.
- Labels (`bug`, `p1`, `frontend` …): GitHub label rounded pill, each
with its own arbitrary color. Use varied real-world label hues.
- Reviewer dots: 8px filled circles, 2px gap, with `✓ ⏳ ○` glyphs only
if you can keep them visually subtle.
## Implementation constraints (paired do / don't)
| Don't | Do |
|---|---|
| Mix light and dark themes | Stay on the light Primer theme (`#f6f8fa` page bg, `#ffffff` cards) |
| Use non-GitHub typography | Use `-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif` exclusively |
| Render avatars as squares or rounded squares | Always circles, with overlap `≤ 6px` for reviewer stacks |
| Use shadows / gradients / glows on chrome | Flat surfaces; differentiate with `#d0d7de` 1px borders |
| Use lorem ipsum | Write real-shaped GitHub copy: PR titles like `feat: orbit briefing card`, branches like `chore/upgrade-deps`, commit subjects under 72 chars |
| Render a CI failure as a normal row | Wrap in a red-bordered card (`#cf222e` border, `#ffebe9` bg) with a red `✗` glyph and run name |
| Render a review request as a normal row | Sit it in the yellow attention block (`#fff8c5` bg, `#d4a72c` border) with reviewer status dots row |
| Use placeholder repo names like `org/repo` | Use `nexu-io/open-design` (this org's actual primary repo) |
| Pluck arbitrary label colors | Use realistic dev-team hues — `bug` red, `enhancement` blue, `documentation` light blue, `frontend` purple |

View file

@ -0,0 +1,770 @@
<!doctype html>
<html lang="en">
<head><script>(function(){
function makeStore(){
var data = {};
var api = {
getItem: function(k){ return Object.prototype.hasOwnProperty.call(data, k) ? data[k] : null; },
setItem: function(k, v){ data[k] = String(v); },
removeItem: function(k){ delete data[k]; },
clear: function(){ data = {}; },
key: function(i){ return Object.keys(data)[i] || null; }
};
Object.defineProperty(api, 'length', { get: function(){ return Object.keys(data).length; } });
return api;
}
function tryShim(name){
var works = false;
try { works = !!window[name] && typeof window[name].getItem === 'function'; void window[name].length; }
catch (_) { works = false; }
if (works) return;
try { Object.defineProperty(window, name, { configurable: true, value: makeStore() }); }
catch (_) { try { window[name] = makeStore(); } catch (__) {} }
}
tryShim('localStorage');
tryShim('sessionStorage');
})();</script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Orbit · GitHub Briefing — 2026-05-06</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
color: #1f2328;
background: #f6f8fa;
}
/* ── GitHub Top Nav ── */
.gh-header {
background: #24292f;
height: 48px;
display: flex;
align-items: center;
padding: 0 16px;
position: sticky;
top: 0;
z-index: 100;
}
.gh-header svg { fill: #ffffff; }
.gh-header-logo { flex-shrink: 0; }
.gh-header-logo svg { width: 32px; height: 32px; }
.gh-header-logo svg:hover { fill: rgba(255,255,255,0.7); }
.gh-header-search {
margin-left: 16px;
flex: 1;
max-width: 540px;
}
.gh-header-search input {
width: 100%;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 6px;
padding: 5px 12px;
font-size: 14px;
color: #ffffff;
outline: none;
font-family: inherit;
}
.gh-header-search input::placeholder { color: rgba(255,255,255,0.4); }
.gh-header-search input:focus {
background: rgba(255,255,255,0.12);
border-color: rgba(255,255,255,0.4);
}
.gh-header-search-slash {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 4px;
padding: 0 5px;
font-size: 11px;
color: rgba(255,255,255,0.4);
line-height: 18px;
}
.gh-header-actions {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.gh-avatar-header {
width: 20px;
height: 20px;
border-radius: 50%;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 600;
color: #ffffff;
cursor: pointer;
}
/* ── Page Layout ── */
.page-container {
max-width: 1280px;
margin: 0 auto;
padding: 24px 32px;
display: flex;
gap: 24px;
}
/* ── Left Sidebar Nav ── */
.sidebar {
width: 256px;
flex-shrink: 0;
}
.sidebar-nav {
list-style: none;
}
.sidebar-nav li button {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 6px;
color: #1f2328;
font-size: 14px;
font-weight: 400;
font-family: inherit;
background: none;
border: none;
width: 100%;
cursor: pointer;
text-align: left;
}
.sidebar-nav li button:hover {
background: rgba(208,215,222,0.32);
}
.sidebar-nav li button.active {
background: rgba(208,215,222,0.48);
font-weight: 600;
}
.sidebar-nav li button svg {
fill: #656d76;
width: 16px;
height: 16px;
flex-shrink: 0;
}
.sidebar-nav li button.active svg { fill: #1f2328; }
.sidebar-count {
margin-left: auto;
background: rgba(208,215,222,0.48);
padding: 0 6px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
color: #1f2328;
line-height: 20px;
}
/* ── Main Content ── */
.main-content {
flex: 1;
min-width: 0;
}
/* ── Section Groups ── */
.event-group {
margin-bottom: 16px;
}
.event-group-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
font-size: 14px;
font-weight: 600;
border: 1px solid #d0d7de;
border-radius: 6px 6px 0 0;
background: #f6f8fa;
}
.event-group-header.warning {
background: #fff8c5;
border-color: #d4a72c;
}
.event-group-header.ci-fail {
background: #ffebe9;
border-color: #cf222e;
}
.event-group-header .count {
background: rgba(208,215,222,0.48);
padding: 0 6px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
line-height: 20px;
}
.event-group-header.warning .count {
background: rgba(154,103,0,0.12);
color: #9a6700;
}
.event-group-header.ci-fail .count {
background: rgba(207,34,46,0.12);
color: #cf222e;
}
/* ── Event Rows ── */
.event-list {
border: 1px solid #d0d7de;
border-top: none;
border-radius: 0 0 6px 6px;
background: #ffffff;
}
.event-row {
padding: 12px 16px;
border-bottom: 1px solid #d0d7de;
display: flex;
align-items: flex-start;
gap: 12px;
}
.event-row:last-child { border-bottom: none; }
.event-icon {
flex-shrink: 0;
width: 16px;
height: 16px;
margin-top: 3px;
}
.event-icon svg { width: 16px; height: 16px; }
.event-body {
flex: 1;
min-width: 0;
}
.event-title-line {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 4px;
font-size: 14px;
}
.event-repo {
color: #656d76;
font-weight: 400;
}
.event-sep { color: #656d76; }
.event-pr-label {
color: #656d76;
}
.event-title-text {
font-weight: 600;
color: #1f2328;
}
.event-title-text a {
color: #0969da;
text-decoration: none;
}
.event-title-text a:hover { text-decoration: underline; }
.event-timestamp {
margin-left: auto;
color: #656d76;
font-size: 12px;
white-space: nowrap;
flex-shrink: 0;
}
.event-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-top: 4px;
font-size: 12px;
color: #656d76;
}
/* ── Status Pills ── */
.pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
line-height: 18px;
white-space: nowrap;
}
.pill-open { background: #1a7f37; color: #ffffff; }
.pill-merged { background: #8250df; color: #ffffff; }
.pill-closed { background: #cf222e; color: #ffffff; }
.pill-draft { background: #6e7781; color: #ffffff; }
.pill svg { width: 12px; height: 12px; fill: currentColor; }
/* ── Labels ── */
.gh-label {
display: inline-block;
padding: 0 7px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
line-height: 20px;
border: 1px solid transparent;
}
.label-backend { background: #ddf4ff; color: #0550ae; border-color: rgba(5,80,174,0.2); }
.label-p1 { background: #ffebe9; color: #a40e26; border-color: rgba(164,14,38,0.2); }
/* ── Reviewer Avatars ── */
.reviewer-stack {
display: flex;
align-items: center;
gap: 0;
}
.reviewer-avatar {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
font-weight: 600;
color: #ffffff;
margin-left: -4px;
position: relative;
}
.reviewer-avatar:first-child { margin-left: 0; }
.reviewer-status {
position: absolute;
bottom: -2px;
right: -2px;
width: 10px;
height: 10px;
border-radius: 50%;
border: 1.5px solid #ffffff;
display: flex;
align-items: center;
justify-content: center;
}
.reviewer-status.approved { background: #1a7f37; }
.reviewer-status.pending { background: #bf8700; }
.reviewer-status.none { background: #d0d7de; }
.reviewer-status svg { width: 6px; height: 6px; fill: #ffffff; }
/* ── Diff Preview ── */
.diff-preview {
margin-top: 8px;
border: 1px solid #d0d7de;
border-radius: 6px;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
font-size: 12px;
line-height: 20px;
overflow: hidden;
}
.diff-header {
background: #f6f8fa;
padding: 4px 12px;
color: #656d76;
font-size: 12px;
border-bottom: 1px solid #d0d7de;
display: flex;
align-items: center;
gap: 6px;
}
.diff-header svg { width: 12px; height: 12px; fill: #656d76; }
.diff-line {
padding: 0 12px;
white-space: pre;
}
.diff-line.add {
background: #dafbe1;
color: #116329;
}
.diff-line.remove {
background: #ffebe9;
color: #82071e;
}
.diff-line.context {
background: #ffffff;
color: #656d76;
}
.diff-line-number {
display: inline-block;
width: 32px;
text-align: right;
color: rgba(31,35,40,0.3);
user-select: none;
padding-right: 8px;
}
/* ── CI Fail Card ── */
.ci-fail-card .event-row {
border-left: 3px solid #cf222e;
}
/* ── Conversation Indicator ── */
.convo-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
background: #fff8c5;
color: #9a6700;
}
.convo-badge svg { width: 12px; height: 12px; fill: currentColor; }
/* ── Footer ── */
.orbit-footer {
text-align: center;
padding: 24px 16px 32px;
font-size: 12px;
color: #656d76;
}
.orbit-footer strong {
color: #1f2328;
font-weight: 600;
}
/* ── Responsive ── */
@media (max-width: 768px) {
.page-container {
flex-direction: column;
padding: 16px;
}
.sidebar { width: 100%; }
}
</style>
</head>
<body>
<!-- ════════ GitHub Header ════════ -->
<header class="gh-header">
<!-- Octocat Logo -->
<a class="gh-header-logo" href="#">
<svg viewBox="0 0 16 16" width="32" height="32">
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"/>
</svg>
</a>
<!-- Search -->
<div class="gh-header-search" style="position:relative;">
<input type="text" placeholder="Type / to search" />
<span class="gh-header-search-slash">/</span>
</div>
<!-- Action buttons -->
<div class="gh-header-actions">
<div class="gh-avatar-header">E</div>
</div>
</header>
<!-- ════════ Page ════════ -->
<div class="page-container">
<!-- ── Sidebar ── -->
<nav class="sidebar" data-od-id="sidebar">
<ul class="sidebar-nav" id="sidebarNav">
<li>
<button class="active" data-filter="all">
<svg viewBox="0 0 16 16"><path d="M1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25V2.75C0 1.784.784 1 1.75 1ZM1.5 2.75v10.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25Z"/></svg>
All
<span class="sidebar-count">5</span>
</button>
</li>
<li>
<button data-filter="participating">
<svg viewBox="0 0 16 16"><path d="M1.5 14.25c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25v12.5ZM0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm9.22 3.72a.749.749 0 0 1 1.06 0l3.5 3.5a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L10 7.31 7.28 10.03a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734Z"/></svg>
Participating
</button>
</li>
<li>
<button data-filter="mentions">
<svg viewBox="0 0 16 16"><path d="M4.75 7.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm3.25.75a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0Zm4 0a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0Z"/><path d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25Z"/></svg>
Mentions
</button>
</li>
<li>
<button data-filter="reviews">
<svg viewBox="0 0 16 16"><path d="M3.5 9.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm5-1a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"/></svg>
Review requests
<span class="sidebar-count">2</span>
</button>
</li>
</ul>
</nav>
<!-- ── Main Content ── -->
<main class="main-content" data-od-id="main-content">
<!-- ═══ Review Requests ═══ -->
<div class="event-group" data-od-id="review-requests" data-category="reviews">
<div class="event-group-header warning">
<svg viewBox="0 0 16 16" width="16" height="16" fill="#9a6700"><path d="M8 2c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6 2.69-6 6-6Zm.25 3a.75.75 0 0 0-1.5 0v3c0 .199.079.39.22.53l2 2a.749.749 0 0 0 1.275-.326.749.749 0 0 0-.215-.734L8.25 7.69Z"/></svg>
Review requests waiting on you
<span class="count">2</span>
</div>
<div class="event-list">
<!-- PR #2371 -->
<div class="event-row">
<div class="event-icon">
<svg viewBox="0 0 16 16" fill="#1a7f37"><path d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z"/></svg>
</div>
<div class="event-body">
<div class="event-title-line">
<span class="event-repo">open-design/web</span>
<span class="event-sep">·</span>
<span class="event-pr-label">PR #2371</span>
<span class="event-sep">·</span>
<span class="pill pill-open">
<svg viewBox="0 0 16 16"><path d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354Z"/></svg>
Open
</span>
<span class="event-title-text"><a href="#">feat: orbit briefing card</a></span>
<span class="event-timestamp">opened 2d ago by marie</span>
</div>
<div class="event-meta">
<!-- Reviewer Avatars -->
<div class="reviewer-stack">
<div class="reviewer-avatar" style="background:#2da44e;" title="alex — approved">A
<span class="reviewer-status approved"><svg viewBox="0 0 16 16"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/></svg></span>
</div>
<div class="reviewer-avatar" style="background:#0969da;" title="sam — approved">S
<span class="reviewer-status approved"><svg viewBox="0 0 16 16"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/></svg></span>
</div>
<div class="reviewer-avatar" style="background:#cf222e;" title="jess — approved">J
<span class="reviewer-status approved"><svg viewBox="0 0 16 16"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/></svg></span>
</div>
<div class="reviewer-avatar" style="background:#8250df;" title="eli — pending">E
<span class="reviewer-status pending"></span>
</div>
<div class="reviewer-avatar" style="background:#bf3989;" title="cody — pending">C
<span class="reviewer-status pending"></span>
</div>
</div>
<span style="color:#656d76;">✓ 3 of 5 reviewers approved · waiting on <strong style="color:#1f2328;">you</strong> + <strong style="color:#1f2328;">cody</strong></span>
</div>
<!-- Diff Preview -->
<div class="diff-preview">
<div class="diff-header">
<svg viewBox="0 0 16 16"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914Z"/></svg>
src/components/OrbitBriefingCard.tsx
</div>
<div class="diff-line context"><span class="diff-line-number">42</span> return (</div>
<div class="diff-line remove"><span class="diff-line-number">43</span>- &lt;div className="briefing-legacy"&gt;</div>
<div class="diff-line add"><span class="diff-line-number">43</span>+ &lt;Card variant="orbit" density="compact"&gt;</div>
<div class="diff-line add"><span class="diff-line-number">44</span>+ &lt;BriefingHeader provider={provider} /&gt;</div>
</div>
</div>
</div>
<!-- PR #2389 -->
<div class="event-row">
<div class="event-icon">
<svg viewBox="0 0 16 16" fill="#1a7f37"><path d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z"/></svg>
</div>
<div class="event-body">
<div class="event-title-line">
<span class="event-repo">open-design/web</span>
<span class="event-sep">·</span>
<span class="event-pr-label">PR #2389</span>
<span class="event-sep">·</span>
<span class="pill pill-open">
<svg viewBox="0 0 16 16"><path d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354Z"/></svg>
Open
</span>
<span class="event-title-text"><a href="#">refactor: skill loader</a></span>
<span class="event-timestamp">1d ago</span>
</div>
<div class="event-meta">
<span class="convo-badge">
<svg viewBox="0 0 16 16"><path d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/></svg>
Conversation needs your reply
</span>
<span style="color:#656d76;">@cody asked a question</span>
</div>
</div>
</div>
</div>
</div>
<!-- ═══ CI / Checks ═══ -->
<div class="event-group ci-fail-card" data-od-id="ci-checks" data-category="ci">
<div class="event-group-header ci-fail">
<svg viewBox="0 0 16 16" width="16" height="16" fill="#cf222e"><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm9.78-2.22-5.5 5.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l5.5-5.5a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"/></svg>
CI / Checks
<span class="count">1</span>
</div>
<div class="event-list">
<div class="event-row">
<div class="event-icon">
<svg viewBox="0 0 16 16" fill="#cf222e"><path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"/></svg>
</div>
<div class="event-body">
<div class="event-title-line">
<span class="event-repo">open-design/web</span>
<span class="event-sep">·</span>
<span style="color:#1f2328;font-weight:600;">main</span>
<span class="event-sep">·</span>
<span style="color:#cf222e;font-weight:600;">✗ test (e2e) failed</span>
<span class="event-timestamp">2h ago</span>
</div>
<div class="event-meta">
<svg viewBox="0 0 16 16" width="12" height="12" fill="#656d76"><path d="M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.25a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"/></svg>
<span>Last commit: <code style="font-family:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;background:#f6f8fa;border:1px solid #d0d7de;padding:1px 5px;border-radius:4px;font-size:0.85em;">chore(deps): bump cheerio to 1.0.2</code></span>
</div>
</div>
</div>
</div>
</div>
<!-- ═══ Issues assigned to you ═══ -->
<div class="event-group" data-od-id="issues" data-category="issues" data-mentions>
<div class="event-group-header">
<svg viewBox="0 0 16 16" width="16" height="16" fill="#1a7f37"><path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"/><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"/></svg>
Issues assigned to you
<span class="count">1</span>
</div>
<div class="event-list">
<div class="event-row">
<div class="event-icon">
<svg viewBox="0 0 16 16" fill="#1a7f37"><path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"/><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"/></svg>
</div>
<div class="event-body">
<div class="event-title-line">
<span class="event-repo">open-design/web</span>
<span class="event-sep">·</span>
<span class="event-pr-label">ENG-148</span>
<span class="event-sep">·</span>
<span class="pill pill-open">
<svg viewBox="0 0 16 16"><path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"/><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"/></svg>
Open
</span>
<span class="event-title-text"><a href="#">Auth middleware refactor</a></span>
<span class="event-timestamp">no activity 5d</span>
</div>
<div class="event-meta">
<span class="gh-label label-backend">backend</span>
<span class="gh-label label-p1">p1</span>
</div>
</div>
</div>
</div>
</div>
<!-- ═══ Activity ═══ -->
<div class="event-group" data-od-id="activity" data-category="activity">
<div class="event-group-header">
<svg viewBox="0 0 16 16" width="16" height="16" fill="#656d76"><path d="M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.25a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"/></svg>
Activity
<span class="count">1</span>
</div>
<div class="event-list">
<div class="event-row">
<div class="event-icon">
<svg viewBox="0 0 16 16" fill="#8250df"><path d="M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z"/></svg>
</div>
<div class="event-body">
<div class="event-title-line">
<span class="event-repo">open-design/web</span>
<span class="event-sep">·</span>
<span class="event-pr-label">PR #2401</span>
<span class="event-sep">·</span>
<span class="pill pill-merged">
<svg viewBox="0 0 16 16"><path d="M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z"/></svg>
Merged
</span>
<span class="event-title-text">merged into main by <strong>bob</strong></span>
<span class="event-timestamp">18h ago</span>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- ════════ Footer ════════ -->
<footer class="orbit-footer">
<strong>Open Orbit</strong> · auto-generated 06:42 · 2026-05-06
</footer>
<script>
(function() {
var nav = document.getElementById('sidebarNav');
var groups = document.querySelectorAll('.event-group');
nav.addEventListener('click', function(e) {
var btn = e.target.closest('button');
if (!btn) return;
// Toggle active
nav.querySelectorAll('button').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
var filter = btn.getAttribute('data-filter');
groups.forEach(function(g) {
if (filter === 'all') {
g.style.display = '';
} else if (filter === 'participating') {
// User participates in reviews + issues assigned
g.style.display = (g.hasAttribute('data-category') &&
(g.dataset.category === 'reviews' || g.dataset.category === 'issues')) ? '' : 'none';
} else if (filter === 'mentions') {
g.style.display = g.hasAttribute('data-mentions') ? '' : 'none';
} else if (filter === 'reviews') {
g.style.display = g.dataset.category === 'reviews' ? '' : 'none';
}
});
});
// Row hover highlight + click-to-open-on-GitHub
const REPO = 'https://github.com/nexu-io/open-design';
function buildUrl(label) {
const t = (label || '').trim();
const prMatch = t.match(/PR\s*#(\d+)/i);
if (prMatch) return REPO + '/pull/' + prMatch[1];
const issueIdMatch = t.match(/^[A-Z]+-(\d+)/);
if (issueIdMatch) return REPO + '/issues?q=' + encodeURIComponent(t);
const hashMatch = t.match(/#(\d+)/);
if (hashMatch) return REPO + '/issues/' + hashMatch[1];
return REPO;
}
document.querySelectorAll('.event-row').forEach(function(row) {
const label = row.querySelector('.event-pr-label')?.textContent;
const url = buildUrl(label);
row.style.cursor = 'pointer';
row.style.transition = 'background 80ms ease-out';
row.addEventListener('mouseenter', function() { row.style.background = '#f6f8fa'; });
row.addEventListener('mouseleave', function() { row.style.background = ''; });
row.addEventListener('click', function(e) {
// Don't override clicks on inner real anchors
if (e.target.closest('a')) return;
window.open(url, '_blank', 'noopener,noreferrer');
});
// Upgrade the title placeholder anchor to a real URL
const titleAnchor = row.querySelector('.event-title-text a');
if (titleAnchor) {
titleAnchor.href = url;
titleAnchor.target = '_blank';
titleAnchor.rel = 'noopener noreferrer';
}
});
// Top logo also points to the repo
const headerLogo = document.querySelector('.gh-header-logo');
if (headerLogo) {
headerLogo.href = REPO;
headerLogo.target = '_blank';
headerLogo.rel = 'noopener noreferrer';
}
})();
</script>
</body>
</html>

163
skills/orbit-gmail/SKILL.md Normal file
View file

@ -0,0 +1,163 @@
---
name: orbit-gmail
description: |
Open Orbit briefing skill — selected by the Orbit pipeline when
Gmail is the user's only connected connector, or when the user
explicitly scopes their daily digest to Gmail. Pulls the past 24
hours of inbox activity (replies awaited, mentions, cc, auto-
categorized bulk) from the user's authenticated Gmail connection
and renders the digest as the Orbit Daily Digest email opened
inside Gmail's reading view. This skill should not be triggered
manually — it is invoked by Orbit's daily-digest scheduler against
live Gmail data.
triggers:
- "gmail briefing"
- "inbox digest"
- "email summary"
- "gmail 简报"
- "邮件摘要"
od:
mode: prototype
platform: desktop
scenario: orbit
featured: 3
preview:
type: html
entry: index.html
design_system:
requires: false
example_prompt: "Generate today's Open Orbit Gmail briefing. Gmail is my only connected connector — pull yesterday's mail and render it as the opened Orbit Daily Digest email inside Gmail's reading view."
---
# Orbit · Gmail Briefing
Single-connector Orbit template scoped to Gmail. The briefing renders
as **the Orbit Daily Digest email already opened** inside Gmail's
reading view — Gmail top header + the email chrome (toolbar / subject
/ sender / digest body / reply bar). There is no left rail, no inbox
list, and no three-pane layout.
## ⚠️ Source-of-truth protocol (read this first)
**Step 1.** Open and read the shipped `example.html` in this folder
before writing any output. That file is the canonical design — your
job is to **reproduce it**, not reinterpret it.
**Step 2.** Mirror the example's structure 1:1:
- Same DOM hierarchy and class names: `<header>` (Gmail top bar) →
`<main class="digest-wrap">``<div class="email-chrome">`
toolbar / subject / sender row / digest body / reply bar.
- The Gmail top header has only the elements present in the example
(hamburger / wordmark / search bar / help / settings / app launcher
/ avatar). **Do not** add a left rail (no Compose button, no system
labels, no Categories tabs, no colored label list).
- **Do not** render an inbox list of other emails. Only the opened
digest email is shown.
- Same digest-body sections in the same order: greeting → summary
strip → 需要处理 → 值得关注 → 仅供知悉 → digest footer.
- Same reply bar at the bottom (回复 / 全部回复 / 转发).
- Same `<script>` block at the end (action-btn / reply-btn link
injection).
**Step 3.** You may refresh mock copy (sender names, subjects, summary
text, times) so it reads as "today", but you must **not** invent
extra UI: no inbox listing, no left rail, no Categories tab strip,
no extra digest sections, no chrome ornaments. If a detail is not
already in `example.html`, it does not belong in your output.
The sections below are a **reference for tokens and visual language**
not a license to extend the page.
## ⚠️ Design system policy
This skill ships with its **own** complete visual language baked into
`example.html` (Gmail / Google Sans / Material chrome). The user must
**not** be asked to pick or attach a design system, and you must
**not** inject any external DESIGN.md tokens into the output.
- If the active project has a design system attached, **ignore it**.
- If the user supplies brand tokens or a Figma file, **ignore them**.
- Use exclusively the colors / fonts / radii defined in `example.html`.
This is a hard constraint: the briefing must read as a real Gmail
page, not as the user's brand.
## Canvas tokens (use these exact values)
```
page bg: #f6f8fc
surface: #ffffff
border: #e0e0e0
text: #202124
text-secondary: #5f6368
text-muted: #80868b
surface-hover: #f1f3f4
red (Gmail): #D93025 /* Compose, important markers, accent */
blue: #1a73e8 /* CTA / link */
yellow: #f4b400 /* important ★ */
green: #0f9d58
search bar bg: #eaf1fb /* light blue-tinted pill */
```
Type stack:
- `'Google Sans', 'Roboto', -apple-system, system-ui, sans-serif`
- Logo wordmark: Google Sans 22px medium
- Body: 14px / line-height 20px
- Email preview: 13px
## Page sections (top to bottom — the page is one column, not a 3-pane app)
1. **Gmail top header** (`<header>`) — full width, white.
Left: hamburger (☰) + Gmail wordmark (`Gmail`, first `G` red).
Center: rounded search bar (`#eaf1fb` bg, search icon left, settings
icon right, placeholder `搜索邮件`).
Right: ❓ help, ⚙ settings, ▦ Google apps launcher, round avatar.
2. **Email chrome** (`<main class="digest-wrap"> <div class="email-chrome">`)
— the opened email lives directly under the header. No left rail,
no inbox list. Sub-blocks in order:
a. **Email toolbar** — back / archive / delete / mark unread / label
/ spacer / prev / next.
b. **Email subject area**`<h1 class="email-subject">` with the
digest subject (e.g. `☀ Eli, 你昨天的 6 封重要邮件 — Open Orbit
Daily`) followed by an inline `Orbit` tag.
c. **Sender row** — round avatar `O` + `Open Orbit
<orbit@opendesign.local>` + 收件人 `我 ▾` + date right-aligned +
reply icon + more icon.
d. **Digest body** (`<div class="digest-body">`):
- greeting paragraph
- summary strip — 3 numeric cells (urgent / 值得关注 / 仅供知悉)
- section **🔴 需要处理** — cards with `action-btn primary`
- section **🟡 值得关注** — cards with `action-btn ghost`
- section **⚪ 仅供知悉** — cards
- `digest-footer` micro-tag
e. **Reply bar** — bottom row with 回复 / 全部回复 / 转发 buttons.
## Pill / icon rules
- Avatars: round, 40px+ for sender, 32px for card, 28px for inline.
- Labels / tags: small rounded pills with no fill (dot + text) **only**
where they appear in the example.
- The single yellow important star (in the subject area or as a tag)
belongs to the Orbit digest only.
## Implementation constraints (paired do / don't)
| Don't | Do |
|---|---|
| Render a left rail (Compose / system labels / colored labels) | Skip the rail entirely; the page is single-column under the header |
| Render an inbox list of other emails | Show only the opened Orbit Daily Digest email |
| Render a Categories tab strip (主要 / 社交 / 推广) | Skip it; the digest occupies the reading view directly |
| Use non-Google typography | Use `'Google Sans', 'Roboto', -apple-system, system-ui, sans-serif` |
| Add drop shadows on the Gmail chrome | Flat surfaces; only the subtle Material 1 elevation when an element is focused |
| Render avatars as squares | Always circles — sender 40px, card 32px, inline 28px |
| Use lorem ipsum | Write real-shaped Gmail copy: "Q3 预算确认", "Login redesign 反馈", senders like Allen Liu / Marie / Nina Park |
| Use dark mode | Stay on Gmail's default light theme (`#f6f8fc` page) |
| Brand the Gmail chrome with Orbit | Orbit branding lives only inside the digest body (subject `Orbit` tag + footer micro-tag) |
| Put yellow important stars on multiple inbox rows | Only the Orbit Daily Digest row can carry the important marker |

View file

@ -0,0 +1,643 @@
<!doctype html>
<html lang="zh-CN">
<head><script>(function(){
function makeStore(){
var data = {};
var api = {
getItem: function(k){ return Object.prototype.hasOwnProperty.call(data, k) ? data[k] : null; },
setItem: function(k, v){ data[k] = String(v); },
removeItem: function(k){ delete data[k]; },
clear: function(){ data = {}; },
key: function(i){ return Object.keys(data)[i] || null; }
};
Object.defineProperty(api, 'length', { get: function(){ return Object.keys(data).length; } });
return api;
}
function tryShim(name){
var works = false;
try { works = !!window[name] && typeof window[name].getItem === 'function'; void window[name].length; }
catch (_) { works = false; }
if (works) return;
try { Object.defineProperty(window, name, { configurable: true, value: makeStore() }); }
catch (_) { try { window[name] = makeStore(); } catch (__) {} }
}
tryShim('localStorage');
tryShim('sessionStorage');
})();</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Open Orbit · Gmail Daily Digest — Eli</title>
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html { height: 100%; }
body {
min-height: 100%;
font-family: 'Google Sans', 'Roboto', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
font-size: 14px;
color: #202124;
background: #f6f8fc;
-webkit-font-smoothing: antialiased;
}
:root {
--red: #D93025;
--blue: #1a73e8;
--yellow: #f4b400;
--green: #0f9d58;
--bg: #f6f8fc;
--white: #ffffff;
--border: #e0e0e0;
--text: #202124;
--text-secondary: #5f6368;
--text-muted: #80868b;
--surface-hover: #f1f3f4;
}
/* ── Gmail Top Bar ── */
.gmail-bar {
position: sticky; top: 0; z-index: 10;
background: var(--white);
border-bottom: 1px solid var(--border);
height: 64px;
display: flex; align-items: center;
padding: 0 24px;
}
.gmail-bar-left {
display: flex; align-items: center; gap: 16px;
}
.hamburger {
width: 40px; height: 40px; border-radius: 50%;
display: grid; place-items: center;
color: var(--text-secondary); cursor: pointer;
}
.hamburger:hover { background: var(--surface-hover); }
.gmail-logo {
display: flex; align-items: center; gap: 4px;
}
.gmail-logo svg { height: 24px; }
.gmail-logo-text {
font-size: 22px; font-weight: 500; color: var(--text-secondary);
font-family: 'Google Sans', sans-serif;
margin-left: 2px;
}
.search-bar {
flex: 1; max-width: 720px; margin: 0 auto;
display: flex; align-items: center; gap: 12px;
background: #eaf1fb; border-radius: 28px;
height: 48px; padding: 0 16px;
transition: background 0.2s, box-shadow 0.2s;
}
.search-bar:hover {
background: var(--white);
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
}
.search-bar svg { width: 20px; height: 20px; color: var(--text-secondary); flex-shrink: 0; }
.search-bar input {
flex: 1; border: none; outline: none; background: transparent;
font-size: 16px; font-family: inherit; color: var(--text);
}
.search-bar input::placeholder { color: var(--text-secondary); }
.gmail-bar-right {
display: flex; align-items: center; gap: 4px; margin-left: auto; padding-left: 16px;
}
.icon-btn {
width: 40px; height: 40px; border-radius: 50%;
display: grid; place-items: center;
color: var(--text-secondary); cursor: pointer;
border: none; background: none;
}
.icon-btn:hover { background: var(--surface-hover); }
.icon-btn svg { width: 20px; height: 20px; }
.app-grid {
display: grid; grid-template-columns: repeat(3, 4px); gap: 3px;
}
.app-grid span { width: 4px; height: 4px; border-radius: 50%; background: var(--text-secondary); }
.user-avatar {
width: 32px; height: 32px; border-radius: 50%;
background: var(--blue); color: white;
display: grid; place-items: center;
font-size: 14px; font-weight: 500; cursor: pointer;
}
/* ── Digest Container ── */
.digest-wrap {
max-width: 800px;
margin: 0 auto;
padding: 32px 24px 64px;
}
/* ── Email Chrome (looks like an opened email) ── */
.email-chrome {
background: var(--white);
border-radius: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 2px 8px rgba(0,0,0,0.04);
overflow: hidden;
}
/* Toolbar row */
.email-toolbar {
display: flex; align-items: center; gap: 2px;
padding: 8px 12px;
border-bottom: 1px solid #f1f3f4;
}
.email-toolbar .icon-btn { width: 36px; height: 36px; }
.email-toolbar .icon-btn svg { width: 18px; height: 18px; }
.toolbar-spacer { flex: 1; }
/* Subject */
.email-subject-area {
padding: 20px 24px 0;
}
.email-subject {
font-size: 22px; font-weight: 400; color: var(--text);
font-family: 'Google Sans', sans-serif;
line-height: 1.35;
}
.email-subject .tag {
display: inline-block; vertical-align: middle;
font-size: 11px; font-weight: 500;
background: #fef7e0; color: #b06000;
padding: 2px 8px; border-radius: 4px;
margin-left: 8px;
}
/* Sender row */
.sender-row {
display: flex; align-items: center; gap: 12px;
padding: 16px 24px;
}
.sender-avatar {
width: 40px; height: 40px; border-radius: 50%;
display: grid; place-items: center;
font-size: 18px; font-weight: 500; color: white;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
flex-shrink: 0;
}
.sender-info { flex: 1; line-height: 1.4; }
.sender-name { font-size: 14px; font-weight: 500; color: var(--text); }
.sender-name span { font-weight: 400; color: var(--text-secondary); }
.sender-to { font-size: 12px; color: var(--text-secondary); }
.sender-date { font-size: 12px; color: var(--text-secondary); white-space: nowrap; }
/* ── Digest Body ── */
.digest-body {
padding: 8px 24px 32px;
font-size: 14px; line-height: 1.7; color: #3c4043;
}
.greeting {
font-size: 15px; color: var(--text); margin-bottom: 28px;
line-height: 1.65;
}
/* Section */
.section { margin-bottom: 28px; }
.section-header {
display: flex; align-items: center; gap: 8px;
font-size: 13px; font-weight: 600; color: var(--text);
text-transform: uppercase; letter-spacing: 0.04em;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid var(--border);
}
.section-header .emoji { font-size: 15px; }
.section-count {
font-size: 11px; font-weight: 500; color: var(--text-secondary);
background: #f1f3f4; padding: 2px 8px; border-radius: 10px;
margin-left: 4px;
}
/* Card */
.card {
display: flex; gap: 14px;
padding: 14px 16px;
border-radius: 12px;
background: #f8f9fa;
margin-bottom: 8px;
transition: background 0.15s;
border: 1px solid transparent;
}
.card:hover { background: #eef2f7; border-color: #e0e4ea; }
.card-avatar {
width: 36px; height: 36px; border-radius: 50%;
display: grid; place-items: center;
font-size: 13px; font-weight: 600; color: white;
flex-shrink: 0;
}
.card-body { flex: 1; min-width: 0; }
.card-top {
display: flex; align-items: baseline; gap: 6px;
margin-bottom: 3px;
}
.card-sender { font-size: 13px; font-weight: 600; color: var(--text); }
.card-role { font-size: 11px; color: var(--text-muted); }
.card-subject {
font-size: 14px; font-weight: 500; color: var(--text);
margin-bottom: 4px;
}
.card-summary {
font-size: 13px; color: var(--text-secondary); line-height: 1.55;
}
.action-btn {
display: inline-flex; align-items: center; gap: 6px;
margin-top: 10px;
padding: 7px 18px;
border-radius: 18px;
font-size: 12px; font-weight: 500;
font-family: 'Google Sans', sans-serif;
border: none; cursor: pointer;
transition: background 0.15s;
}
.action-btn.primary { background: var(--blue); color: white; }
.action-btn.primary:hover { background: #1765cc; }
.action-btn.ghost {
background: transparent; color: var(--blue);
border: 1px solid #dadce0;
}
.action-btn.ghost:hover { background: #f6f8fc; }
/* Collapsed row */
.collapsed {
display: flex; align-items: center; gap: 12px;
padding: 12px 16px;
border-radius: 12px;
background: #f8f9fa;
margin-bottom: 8px;
font-size: 13px; color: var(--text-secondary);
cursor: pointer;
border: 1px solid transparent;
}
.collapsed:hover { background: #eef2f7; border-color: #e0e4ea; }
.collapsed-icon {
width: 36px; height: 36px; border-radius: 50%;
background: #24292e; color: white;
display: grid; place-items: center;
font-size: 13px; font-weight: 600; flex-shrink: 0;
}
.collapsed-text { flex: 1; }
.collapsed-title { font-weight: 500; color: var(--text); margin-bottom: 2px; }
.highlight { color: var(--blue); font-weight: 500; }
/* Priority indicator on section header */
.priority-high { border-bottom-color: var(--red); }
.priority-mid { border-bottom-color: var(--yellow); }
.priority-low { border-bottom-color: #dadce0; }
/* Footer */
.digest-footer {
margin-top: 36px; padding-top: 20px;
border-top: 1px solid #e8eaed;
display: flex; flex-direction: column; align-items: center; gap: 12px;
}
.orbit-badge {
display: inline-flex; align-items: center; gap: 6px;
font-size: 11px; color: var(--text-muted);
background: #f8f9fa; padding: 6px 14px; border-radius: 14px;
}
.orbit-dot {
width: 14px; height: 14px; border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
}
.footer-links {
display: flex; gap: 16px;
font-size: 11px;
}
.footer-links a {
color: var(--text-muted); text-decoration: none;
}
.footer-links a:hover { color: var(--blue); }
/* ── Summary strip at top of digest ── */
.summary-strip {
display: flex; gap: 0;
margin-bottom: 24px;
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border);
}
.summary-cell {
flex: 1;
padding: 16px;
text-align: center;
background: var(--white);
border-right: 1px solid var(--border);
}
.summary-cell:last-child { border-right: none; }
.summary-num {
font-size: 28px; font-weight: 500; color: var(--text);
font-family: 'Google Sans', sans-serif;
line-height: 1;
margin-bottom: 4px;
}
.summary-label {
font-size: 11px; color: var(--text-muted);
text-transform: uppercase; letter-spacing: 0.05em;
}
.summary-cell.urgent .summary-num { color: var(--red); }
/* ── Reply Bar ── */
.reply-bar {
display: flex; align-items: center; gap: 8px;
padding: 14px 24px;
border-top: 1px solid #e8eaed;
}
.reply-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 8px 20px;
border-radius: 18px;
border: 1px solid #dadce0;
background: var(--white);
font-size: 13px; font-weight: 500; color: #3c4043;
cursor: pointer; font-family: 'Google Sans', sans-serif;
transition: background 0.15s;
}
.reply-btn:hover { background: var(--surface-hover); }
.reply-btn svg { width: 16px; height: 16px; color: var(--text-secondary); }
/* ── Scrollbar ── */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #dadce0; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #bdc1c6; }
</style>
</head>
<body>
<!-- ═══ Gmail Top Bar ═══ -->
<header class="gmail-bar">
<div class="gmail-bar-left">
<div class="hamburger">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg>
</div>
<div class="gmail-logo">
<svg viewBox="0 0 24 24" width="28" height="28" fill="none">
<path d="M2 6a2 2 0 0 1 2-4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6z" fill="none"/>
<rect x="2" y="4" width="20" height="16" rx="2" fill="#EA4335" opacity="0.12"/>
<path d="M22 6l-10 7L2 6" stroke="#EA4335" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="2" y="4" width="20" height="16" rx="2" stroke="#EA4335" stroke-width="1.5" fill="none"/>
</svg>
<span class="gmail-logo-text">Gmail</span>
</div>
</div>
<div class="search-bar">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
<input type="text" placeholder="搜索邮件">
<svg viewBox="0 0 24 24" fill="currentColor" style="cursor:pointer;"><path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/></svg>
</div>
<div class="gmail-bar-right">
<button class="icon-btn" title="帮助">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>
</button>
<button class="icon-btn" title="设置">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
</button>
<div class="icon-btn" title="Google 应用">
<div class="app-grid">
<span></span><span></span><span></span>
<span></span><span></span><span></span>
<span></span><span></span><span></span>
</div>
</div>
<div class="user-avatar">E</div>
</div>
</header>
<!-- ═══ Digest ═══ -->
<main class="digest-wrap">
<div class="email-chrome">
<!-- Toolbar -->
<div class="email-toolbar">
<button class="icon-btn" title="返回" style="width:36px;height:36px;">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<button class="icon-btn" title="归档" style="width:36px;height:36px;">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M20.54 5.23l-1.39-1.68C18.88 3.21 18.47 3 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 5.99 3 6.5V19c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6.5c0-.51-.17-.93-.46-1.27zM12 17.5L6.5 12H10v-2h4v2h3.5L12 17.5zM5.12 5l.81-1h12l.94 1H5.12z"/></svg>
</button>
<button class="icon-btn" title="删除" style="width:36px;height:36px;">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
</button>
<button class="icon-btn" title="标记未读" style="width:36px;height:36px;">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>
</button>
<button class="icon-btn" title="标签" style="width:36px;height:36px;">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M17.63 5.84C17.27 5.33 16.67 5 16 5L5 5.01C3.9 5.01 3 5.9 3 7v10c0 1.1.9 1.99 2 1.99L16 19c.67 0 1.27-.33 1.63-.84L22 12l-4.37-6.16z"/></svg>
</button>
<div class="toolbar-spacer"></div>
<button class="icon-btn" style="width:36px;height:36px;">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>
</button>
<button class="icon-btn" style="width:36px;height:36px;">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
</button>
</div>
<!-- Subject -->
<div class="email-subject-area">
<h1 class="email-subject">
☀ Eli, 你昨天的 6 封重要邮件 — Open Orbit Daily
<span class="tag">Orbit</span>
</h1>
</div>
<!-- Sender -->
<div class="sender-row">
<div class="sender-avatar">O</div>
<div class="sender-info">
<div class="sender-name">Open Orbit <span>&lt;orbit@opendesign.local&gt;</span></div>
<div class="sender-to">收件人:我 ▾</div>
</div>
<div class="sender-date">2026年5月6日 06:42</div>
<button class="icon-btn" title="回复" style="width:36px;height:36px;">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M10 9V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z"/></svg>
</button>
<button class="icon-btn" title="更多" style="width:36px;height:36px;">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
</button>
</div>
<!-- ═══ Digest Content ═══ -->
<div class="digest-body">
<div class="greeting">
早上好 Eli 👋<br>
以下是你昨天5月5日的 Gmail 简报。共 <strong>6 封值得关注</strong>,已按优先级分组。
</div>
<!-- Summary strip -->
<div class="summary-strip">
<div class="summary-cell urgent">
<div class="summary-num">2</div>
<div class="summary-label">需要处理</div>
</div>
<div class="summary-cell">
<div class="summary-num">2</div>
<div class="summary-label">值得关注</div>
</div>
<div class="summary-cell">
<div class="summary-num">2</div>
<div class="summary-label">仅供知悉</div>
</div>
</div>
<!-- ── Section 1: 需要处理 ── -->
<div class="section">
<div class="section-header priority-high">
<span class="emoji">🔴</span>
需要处理
<span class="section-count">2</span>
</div>
<div class="card">
<div class="card-avatar" style="background: #1a73e8;">A</div>
<div class="card-body">
<div class="card-top">
<span class="card-sender">Allen Liu</span>
<span class="card-role">CFO</span>
</div>
<div class="card-subject">Q3 预算确认</div>
<div class="card-summary">财务已审核通过 Q3 预算方案,倾向同意当前版本。等你 sign-off 后即可进入执行阶段。</div>
<button class="action-btn primary">查看邮件并回复</button>
</div>
</div>
<div class="card">
<div class="card-avatar" style="background: #0f9d58;">N</div>
<div class="card-body">
<div class="card-top">
<span class="card-sender">Nina Park</span>
<span class="card-role">客户</span>
</div>
<div class="card-subject">Login redesign 反馈</div>
<div class="card-summary">Nina 对新 login 方案提了 3 个问题:①密码重置流程 ②第三方登录按钮位置 ③无障碍对比度。建议尽快回复。</div>
<button class="action-btn primary">查看邮件并回复</button>
</div>
</div>
</div>
<!-- ── Section 2: 值得关注 ── -->
<div class="section">
<div class="section-header priority-mid">
<span class="emoji">🟡</span>
值得关注
<span class="section-count">2</span>
</div>
<div class="card">
<div class="card-avatar" style="background: #e91e63;">M</div>
<div class="card-body">
<div class="card-top">
<span class="card-sender">Marie</span>
</div>
<div class="card-subject">设计评审纪要</div>
<div class="card-summary">Marie 在纪要中 @了你和 Bob附件包含昨天设计评审的要点。无需回复仅供确认。</div>
<button class="action-btn ghost">查看原文</button>
</div>
</div>
<div class="card">
<div class="card-avatar" style="background: #ff9800;"></div>
<div class="card-body">
<div class="card-top">
<span class="card-sender">招聘团队</span>
</div>
<div class="card-subject">候选人 Sarah 二面安排</div>
<div class="card-summary">Sarah Chen 的二面已排好cc 你作为面试官之一。时间待确认。</div>
<button class="action-btn ghost">查看原文</button>
</div>
</div>
</div>
<!-- ── Section 3: 仅供知悉 ── -->
<div class="section">
<div class="section-header priority-low">
<span class="emoji"></span>
仅供知悉
<span class="section-count">2</span>
</div>
<div class="collapsed">
<div class="collapsed-icon">G</div>
<div class="collapsed-text">
<div class="collapsed-title">GitHub 通知摘要</div>
<div>已折叠 12 封通知 · <span class="highlight">1 条值得看PR #347 已合并到 main</span></div>
</div>
</div>
<div class="card">
<div class="card-avatar" style="background: #5f6368;"></div>
<div class="card-body">
<div class="card-top">
<span class="card-sender">公司全员</span>
</div>
<div class="card-subject">本周 Town Hall 提醒</div>
<div class="card-summary">本周四下午 3:00 Town Hall议题包括 Q3 目标回顾和下半年规划。</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="digest-footer">
<div class="orbit-badge">
<span class="orbit-dot"></span>
已使用 Open Orbit 自动整理 · 2026-05-06 06:42 生成
</div>
</div>
</div>
<!-- Reply bar -->
<div class="reply-bar">
<button class="reply-btn">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M10 9V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z"/></svg>
回复
</button>
<button class="reply-btn">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 8V5l-7 7 7 7v-3l-4-4 4-4zm6 1V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z"/></svg>
全部回复
</button>
<button class="reply-btn">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M14 9V5l7 7-7 7v-4.1c-5 0-8.5 1.6-11 5.1 1-5 4-10 11-11z"/></svg>
转发
</button>
</div>
</div>
</main>
<script>
// Open the matching Gmail thread in a new tab when an action button or
// reply-bar button is clicked. Uses Gmail's #search/from: deep link
// built from the sender's display name.
function openInGmail(sender, subject) {
const q = sender ? `from:${sender}` : (subject || '');
const url = 'https://mail.google.com/mail/u/0/#search/' + encodeURIComponent(q);
window.open(url, '_blank', 'noopener,noreferrer');
}
document.querySelectorAll('.card').forEach(card => {
const sender = card.querySelector('.card-sender')?.textContent?.trim();
const subject = card.querySelector('.card-subject')?.textContent?.trim();
card.querySelectorAll('.action-btn').forEach(btn => {
btn.style.cursor = 'pointer';
btn.addEventListener('click', e => {
e.preventDefault();
openInGmail(sender, subject);
});
});
});
document.querySelectorAll('.reply-btn').forEach(btn => {
btn.style.cursor = 'pointer';
btn.addEventListener('click', e => {
e.preventDefault();
window.open('https://mail.google.com/mail/u/0/#inbox', '_blank', 'noopener,noreferrer');
});
});
</script>
</body>
</html>

View file

@ -0,0 +1,170 @@
---
name: orbit-linear
description: |
Open Orbit briefing skill — selected by the Orbit pipeline when
Linear is the user's only connected connector, or when the user
explicitly scopes their daily digest to Linear. Pulls the past 24
hours of issue movement, status changes, assignments, and cycle
progress from the user's authenticated Linear connection and renders
the digest in Linear's native Inbox + cycle-progress visual language.
This skill should not be triggered manually — it is invoked by
Orbit's daily-digest scheduler against live Linear data.
triggers:
- "linear briefing"
- "linear digest"
- "issue digest"
- "linear 简报"
- "issue 汇总"
od:
mode: prototype
platform: desktop
scenario: orbit
featured: 4
preview:
type: html
entry: index.html
design_system:
requires: false
example_prompt: "Generate today's Open Orbit Linear briefing. Linear is my only connected connector — pull yesterday's issue movement, cycle progress, status changes, and assignments and render them in Linear's native Inbox layout."
---
# Orbit · Linear Briefing
Single-connector Orbit template scoped to Linear.
## ⚠️ Source-of-truth protocol (read this first)
**Step 1.** Open and read the shipped `example.html` in this folder
before writing any output. That file is the canonical design — your
job is to **reproduce it**, not reinterpret it.
**Step 2.** Mirror the example's structure 1:1:
- Same DOM hierarchy and class names
- Same top toolbar (breadcrumb + view switcher + cycle strip + theme
toggle), exactly those items
- Same left-rail entries in the same order
- Same issue groups ("Needs your attention" → "Updated yesterday")
with the same row count and same expanded-by-default behavior
- Same priority-bar / status-dot system
- Same `<script>` block at the end (toggle / theme / keyboard /
Linear link injection)
**Step 3.** You may refresh mock values (issue identifiers, titles,
labels, ages, assignees) so they read as "today", but you must
**not** add extra rail entries, extra groups, extra fields in the
preview pane, or any chrome ornaments not already in `example.html`.
The sections below are a **reference for tokens and visual language**
not a license to extend the page.
## ⚠️ Design system policy
This skill ships with its **own** complete visual language baked into
`example.html` (Linear's signature compact UI). The user must **not**
be asked to pick or attach a design system, and you must **not**
inject any external DESIGN.md tokens into the output.
- If the active project has a design system attached, **ignore it**.
- If the user supplies brand tokens or a Figma file, **ignore them**.
- Use exclusively the colors / fonts / radii defined in `example.html`.
This is a hard constraint: the briefing must read as a real Linear
page, not as the user's brand.
## Canvas tokens — light theme (default to ship)
```
page bg: #f4f5f6
surface: #ffffff
ink: #1b1c1f
ink-2: #37393e
ink-3 (muted): #6c6f78
ink-4: #9ea1a9
border: rgba(0,0,0,0.06)
border-card: rgba(0,0,0,0.08)
border-strong: rgba(0,0,0,0.12)
hover row: rgba(0,0,0,0.025)
active row: rgba(0,0,0,0.05)
accent: #5e6ad2
accent-bg: rgba(94,106,210,0.06)
attention accent: #c77d1a /* "needs attention" group */
attention bg: rgba(212,148,14,0.06)
shadow-card: 0 1px 2px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.05)
```
Status dot palette (must use exactly these):
```
backlog: #9ea1a9 /* gray, hollow ring */
todo: #d4940e /* yellow, dashed ring */
progress: #2b80c5 /* blue, partial ring */
review: #8759c7 /* purple, partial ring */
done: #1a8d3a /* green, filled */
canceled: #6c6f78 /* gray with strike */
```
Priority icon = 4 small vertical bars, height ascending.
Filled bars indicate level: 0 None → 4 Urgent.
Urgent uses `#d4513a`; High uses `#c77d1a`; Medium/Low use `#505259`.
Type stack:
- `'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display', system-ui, sans-serif`
- Mono: `'Berkeley Mono', ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, monospace`
- Sizes: nav 13px, row title 13.5px, meta 12px, headers 11px caps with letter-spacing 0.04em
## Page sections
1. **Top toolbar** — single row, 44px tall, no shadow, hairline border-bottom.
Left: breadcrumb `Orbit Daily Digest May 6` (13px, `…›…` separators
in `ink-4`). Then a thin divider, then `▼ My issues` view switcher.
Right: `🔍 search`, `+ new`, `▦ display options`, avatar.
2. **Cycle progress strip** — slot to the right of the breadcrumb area,
one line: `Cycle 12 · 60% complete · 3 days left`. Render as 11px caps
with a tiny inline progress bar (60px wide, 4px tall, accent fill).
3. **Three-column main**:
- **Left nav** (240px): vertical, no background — items at 13px.
Sections: `Inbox · My issues · Active · Backlog · All issues`,
then a divider, then `📋 Triage · 🚫 Canceled · ✅ Completed`.
Active row: `accent-bg` background, `accent` ink.
Bottom: a tiny `Open Orbit · auto-generated 06:42` muted line.
- **Issue list** (flex 1): two grouped sections.
- **Needs your attention** — header in `attention accent` 11px caps;
group block has `attention bg` very subtle background.
Rows include: assigned + stale issues, high/urgent priority.
- **Updated yesterday** — header 11px caps muted; rows of status
changes and completions.
Each row is one tight line:
`[priority bars] [identifier ENG-148] [status dot] [title……………] [labels] [cycle chip] [assignee avatar]`
Row height ~36px. Hover = `hover row` color.
- **Issue preview** (360px right): the pre-selected issue.
Title large (16px medium); ID + status pill below; description
paragraphs; an Activity stream (small avatar + verbed action +
timestamp); Labels chips at bottom; Cycle chip; Assignees row.
## Identifier / chip rules
- Issue IDs (e.g. `ENG-148`) are mono, 12px, `ink-3`.
- Labels: rounded pill with a 4px colored dot, label text, optional ✕.
Hue per label is arbitrary, choose realistic dev-team colors.
- Cycle chip: small rounded box `Cycle 12` with hairline border.
- Status dots: 14px circles with internal ring/fill per state above.
- Priority bars: 4 short vertical bars right of identifier, fill bars
per level.
## Implementation constraints (paired do / don't)
| Don't | Do |
|---|---|
| Add shadows beyond the listed `shadow-card` token | Use only `0 1px 2px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.05)` for cards |
| Use bright colors outside the status palette | Use only the documented status hues (Backlog gray / Todo yellow / Progress blue / Review purple / Done green) and the `#5e6ad2` accent |
| Use sans-serif typography that isn't Inter | Use `'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display', system-ui, sans-serif` |
| Use airy row heights | Keep rows under 40px (target ~36px) — Linear is signature-dense |
| Use lorem ipsum | Write real-shaped Linear copy: identifiers like `ENG-148`, `DES-22`, `INF-9`; cycle names like `Cycle 12`; titles like "Auth middleware refactor" |
| Render avatars as squares | Always circles, 1824px |
| Ship the dark theme | Render the light theme — `#f4f5f6` page, `#ffffff` cards |
| Use placeholder team prefixes like `T-1` | Use real-shaped team prefixes: `ENG / DES / INF / OPS` |

View file

@ -0,0 +1,571 @@
<!doctype html>
<html lang="en" data-theme="light">
<head><script>(function(){
function makeStore(){
var data = {};
var api = {
getItem: function(k){ return Object.prototype.hasOwnProperty.call(data, k) ? data[k] : null; },
setItem: function(k, v){ data[k] = String(v); },
removeItem: function(k){ delete data[k]; },
clear: function(){ data = {}; },
key: function(i){ return Object.keys(data)[i] || null; }
};
Object.defineProperty(api, 'length', { get: function(){ return Object.keys(data).length; } });
return api;
}
function tryShim(name){
var works = false;
try { works = !!window[name] && typeof window[name].getItem === 'function'; void window[name].length; }
catch (_) { works = false; }
if (works) return;
try { Object.defineProperty(window, name, { configurable: true, value: makeStore() }); }
catch (_) { try { window[name] = makeStore(); } catch (__) {} }
}
tryShim('localStorage');
tryShim('sessionStorage');
})();</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Orbit · Daily Digest · May 6</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display', system-ui, sans-serif;
--font-mono: 'Berkeley Mono', ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, monospace;
}
[data-theme="light"] {
--bg: #f4f5f6; --surface: #ffffff; --surface-raised: #ffffff;
--surface-2: #f0f1f3; --surface-inset: #f8f9fa;
--fg: #1b1c1f; --fg-2: #37393e; --fg-3: #6c6f78; --fg-4: #9ea1a9;
--border: rgba(0,0,0,0.06); --border-card: rgba(0,0,0,0.08);
--border-strong: rgba(0,0,0,0.12); --border-focus: rgba(94,106,210,0.35);
--hover: rgba(0,0,0,0.025); --active: rgba(0,0,0,0.05);
--accent: #5e6ad2; --accent-light: #6c78e2; --accent-bg: rgba(94,106,210,0.06);
--tag: rgba(0,0,0,0.045); --tag-border: rgba(0,0,0,0.06);
--shadow-card: 0 1px 2px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.05);
--shadow-card-hover: 0 2px 8px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.08);
--shadow-elevated: 0 4px 16px rgba(0,0,0,0.08), 0 0 0 1px rgba(0,0,0,0.06);
--status-backlog: #9ea1a9; --status-todo: #d4940e;
--status-progress: #2b80c5; --status-review: #8759c7; --status-done: #1a8d3a;
--pri-on: #505259; --pri-off: rgba(0,0,0,0.08);
--pri-high: #c77d1a; --pri-urgent: #d4513a;
--scroll-thumb: rgba(0,0,0,0.08);
--attention-accent: #c77d1a; --attention-bg: rgba(212,148,14,0.06);
--code-bg: rgba(0,0,0,0.04);
}
[data-theme="dark"] {
--bg: #0f0f12; --surface: #18181c; --surface-raised: #1e1e23;
--surface-2: #242429; --surface-inset: #141417;
--fg: #ededef; --fg-2: #c5c7cc; --fg-3: #8b8d95; --fg-4: #5f616a;
--border: rgba(255,255,255,0.06); --border-card: rgba(255,255,255,0.07);
--border-strong: rgba(255,255,255,0.11); --border-focus: rgba(94,106,210,0.45);
--hover: rgba(255,255,255,0.035); --active: rgba(255,255,255,0.06);
--accent: #7b83eb; --accent-light: #8b93f5; --accent-bg: rgba(123,131,235,0.08);
--tag: rgba(255,255,255,0.055); --tag-border: rgba(255,255,255,0.07);
--shadow-card: 0 1px 2px rgba(0,0,0,0.2), 0 0 0 1px rgba(255,255,255,0.05);
--shadow-card-hover: 0 2px 8px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.08);
--shadow-elevated: 0 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.06);
--status-backlog: #5f616a; --status-todo: #e5a72f;
--status-progress: #4da0ee; --status-review: #a87ce0; --status-done: #2eae4e;
--pri-on: #b8bac2; --pri-off: rgba(255,255,255,0.08);
--pri-high: #e5a72f; --pri-urgent: #ef6b4a;
--scroll-thumb: rgba(255,255,255,0.08);
--attention-accent: #e5a72f; --attention-bg: rgba(229,167,47,0.06);
--code-bg: rgba(255,255,255,0.06);
}
html { background: var(--bg); }
body {
font-family: var(--font-sans); font-feature-settings: "cv01", "ss03";
color: var(--fg); font-size: 14px; line-height: 1.5;
-webkit-font-smoothing: antialiased; min-height: 100vh;
}
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--scroll-thumb); border-radius: 3px; }
.page { max-width: 680px; margin: 0 auto; padding: 56px 20px 96px; }
.header { margin-bottom: 40px; }
.header-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 32px; }
.brand { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 510; color: var(--fg-4); letter-spacing: -0.01em; }
.brand .logo { width: 18px; height: 18px; opacity: 0.45; }
.brand .sep { color: var(--fg-4); opacity: 0.4; margin: 0 1px; }
.brand .current { color: var(--fg-3); }
.theme-btn {
width: 28px; height: 28px; border-radius: 8px;
background: transparent; border: 1px solid var(--border);
color: var(--fg-4); cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 13px; transition: all 0.15s;
}
.theme-btn:hover { background: var(--hover); border-color: var(--border-strong); color: var(--fg-3); }
.greeting { margin-bottom: 24px; }
.greeting h1 {
font-size: 24px; font-weight: 590; letter-spacing: -0.4px;
color: var(--fg); line-height: 1.2; margin-bottom: 4px;
}
.greeting p { font-size: 13px; color: var(--fg-4); font-weight: 400; letter-spacing: -0.01em; }
.cycle-card {
display: flex; align-items: center; gap: 14px;
padding: 14px 18px; border-radius: 12px;
background: var(--surface-raised);
box-shadow: var(--shadow-card);
}
.cycle-icon {
width: 32px; height: 32px; border-radius: 8px;
background: var(--accent-bg); display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.cycle-icon svg { color: var(--accent); }
.cycle-info { flex: 1; min-width: 0; }
.cycle-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.cycle-label { font-size: 13px; font-weight: 590; color: var(--fg); letter-spacing: -0.01em; }
.cycle-meta { font-size: 12px; color: var(--fg-4); font-weight: 400; }
.cycle-track { height: 4px; border-radius: 2px; background: var(--pri-off); overflow: hidden; }
.cycle-fill { height: 100%; border-radius: 2px; background: var(--accent); transition: width 0.5s cubic-bezier(0.4,0,0.2,1); }
.section { margin-bottom: 36px; }
.section-header {
display: flex; align-items: center; gap: 7px;
font-size: 11px; font-weight: 590; color: var(--fg-4);
text-transform: uppercase; letter-spacing: 0.5px;
margin-bottom: 8px; padding: 0 4px;
}
.section-header .dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
.section.attention .dot { background: var(--attention-accent); }
.section.updates .dot { background: var(--fg-4); opacity: 0.5; }
.section-header .count {
font-weight: 400; color: var(--fg-4); opacity: 0.7;
text-transform: none; letter-spacing: 0;
}
.issue-list { display: flex; flex-direction: column; gap: 4px; }
.issue {
display: flex; align-items: flex-start; gap: 12px;
padding: 12px 16px; border-radius: 10px;
background: var(--surface-raised);
box-shadow: var(--shadow-card);
cursor: pointer; transition: all 0.12s ease;
position: relative;
}
.issue:hover { box-shadow: var(--shadow-card-hover); transform: translateY(-0.5px); }
.issue.expanded { box-shadow: var(--shadow-card-hover); }
.issue-status { padding-top: 2px; flex-shrink: 0; width: 16px; height: 16px; }
.issue-status svg { display: block; }
.issue-body { flex: 1; min-width: 0; }
.issue-top { display: flex; align-items: baseline; gap: 8px; margin-bottom: 1px; }
.issue-id {
font-family: var(--font-mono); font-size: 11.5px; color: var(--fg-4);
flex-shrink: 0; font-weight: 400; letter-spacing: -0.02em;
}
.issue-title {
font-size: 13.5px; font-weight: 510; color: var(--fg);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
letter-spacing: -0.01em;
}
.issue-sub {
font-size: 12px; color: var(--fg-4); line-height: 1.5;
margin-top: 1px; letter-spacing: -0.005em;
}
.issue-sub strong { color: var(--fg-3); font-weight: 510; }
.issue-right {
display: flex; flex-direction: column; align-items: flex-end; gap: 5px;
flex-shrink: 0; padding-top: 1px;
}
.issue-time { font-size: 11px; color: var(--fg-4); white-space: nowrap; font-weight: 400; }
.pri { display: flex; gap: 1.5px; align-items: flex-end; }
.pri i { width: 3px; border-radius: 0.75px; background: var(--pri-off); display: block; font-style: normal; }
.pri i.on { background: var(--pri-on); }
.pri.high i.on { background: var(--pri-high); }
.pri.urgent i.on { background: var(--pri-urgent); }
.pri i:nth-child(1) { height: 5px; }
.pri i:nth-child(2) { height: 7px; }
.pri i:nth-child(3) { height: 9px; }
.pri i:nth-child(4) { height: 11px; }
.tags { display: flex; gap: 4px; margin-top: 7px; flex-wrap: wrap; }
.pill {
display: inline-flex; align-items: center; gap: 4px;
padding: 2px 8px; border-radius: 9999px; font-size: 11px; font-weight: 510;
background: var(--tag); color: var(--fg-3); letter-spacing: -0.01em;
}
.pill .d { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
.issue-detail {
display: none; margin-top: 12px; padding-top: 12px;
border-top: 1px solid var(--border);
}
.issue.expanded .issue-detail { display: block; }
.detail-desc {
font-size: 13px; color: var(--fg-2); line-height: 1.65;
letter-spacing: -0.005em; margin-bottom: 14px;
}
.detail-desc code {
font-family: var(--font-mono); font-size: 12px;
padding: 2px 6px; border-radius: 4px;
background: var(--code-bg); color: var(--fg);
}
.props {
display: grid; grid-template-columns: 76px 1fr; gap: 8px 0;
font-size: 12px; margin-bottom: 16px;
padding: 10px 12px; border-radius: 8px;
background: var(--surface-inset);
}
.props .k { color: var(--fg-4); font-weight: 400; padding-top: 1px; }
.props .v { color: var(--fg-2); font-weight: 510; display: flex; align-items: center; gap: 5px; }
.activity-label {
font-size: 11px; font-weight: 590; color: var(--fg-4);
text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 8px;
}
.act {
display: flex; align-items: flex-start; gap: 8px;
padding: 6px 0; position: relative;
}
.act + .act { border-top: 1px solid var(--border); }
.act-av {
width: 22px; height: 22px; border-radius: 50%; flex-shrink: 0;
background: var(--tag); display: flex; align-items: center; justify-content: center;
font-size: 10px; font-weight: 590; color: var(--fg-3);
}
.act-av.orbit { background: var(--accent-bg); color: var(--accent); }
.act-text { font-size: 12px; color: var(--fg-3); line-height: 1.5; flex: 1; }
.act-text strong { color: var(--fg-2); font-weight: 510; }
.act-text .t { color: var(--fg-4); font-size: 11px; margin-left: 2px; }
.footer {
margin-top: 52px; padding-top: 16px; border-top: 1px solid var(--border);
display: flex; align-items: center; justify-content: space-between;
font-size: 11px; color: var(--fg-4); letter-spacing: -0.005em;
}
.footer .mark { display: flex; align-items: center; gap: 5px; }
.footer .mark svg { opacity: 0.35; }
.kb {
position: fixed; bottom: 14px; right: 14px;
display: flex; gap: 10px; align-items: center;
background: var(--surface-raised); box-shadow: var(--shadow-elevated);
border-radius: 10px; padding: 6px 12px;
font-size: 11px; color: var(--fg-4); z-index: 100;
}
.kb kbd {
font-family: var(--font-sans); font-size: 10px; font-weight: 590;
padding: 2px 5px; border-radius: 4px;
background: var(--surface-2); color: var(--fg-4);
line-height: 1.3; letter-spacing: 0;
}
</style>
</head>
<body>
<div class="page">
<div class="header">
<div class="header-top">
<div class="brand">
<svg class="logo" width="18" height="18" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="7" stroke="currentColor" stroke-width="1.2" opacity="0.5"/>
<circle cx="9" cy="9" r="2" fill="currentColor" opacity="0.5"/>
<ellipse cx="9" cy="9" rx="7" ry="3.5" stroke="currentColor" stroke-width="1" opacity="0.3" transform="rotate(-30 9 9)"/>
</svg>
Orbit <span class="sep">/</span> <span class="current">Daily Digest</span>
</div>
<button class="theme-btn" id="themeToggle" title="Toggle theme (T)"></button>
</div>
<div class="greeting">
<h1>Good morning, Eli</h1>
<p>Tuesday, May 6, 2026 — here's what changed in Linear overnight.</p>
</div>
<div class="cycle-card">
<div class="cycle-icon">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
<circle cx="8" cy="8" r="5.5"/><path d="M8 4.5v3.5l2.5 1.5"/>
</svg>
</div>
<div class="cycle-info">
<div class="cycle-top">
<span class="cycle-label">Cycle 12</span>
<span class="cycle-meta">60% complete · 3 days left</span>
</div>
<div class="cycle-track"><div class="cycle-fill" style="width:60%"></div></div>
</div>
</div>
</div>
<div class="section attention">
<div class="section-header">
<span class="dot"></span> Needs your attention <span class="count">3</span>
</div>
<div class="issue-list">
<div class="issue expanded" data-id="0" onclick="toggle(this)">
<div class="issue-status">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="5.5" stroke="var(--status-progress)" stroke-width="1.8" stroke-dasharray="17.3 17.3" stroke-dashoffset="-8.65" stroke-linecap="round"/>
</svg>
</div>
<div class="issue-body">
<div class="issue-top">
<span class="issue-id">ENG-148</span>
<span class="issue-title">Auth middleware refactor</span>
</div>
<div class="issue-sub">Assigned to <strong>you</strong> · 5 days without update</div>
<div class="tags">
<span class="pill"><span class="d" style="background:#c77d1a"></span>backend</span>
<span class="pill"><span class="d" style="background:#d4513a"></span>auth</span>
</div>
<div class="issue-detail">
<p class="detail-desc">Refactor the auth middleware to use the new <code>session-v2</code> token format. The current implementation relies on legacy JWT claims incompatible with the updated identity service. Migrate all routes under <code>/api/v3/*</code> and ensure backward compat for mobile clients still on v2.</p>
<div class="props">
<span class="k">Priority</span>
<span class="v"><span class="pri high"><i class="on"></i><i class="on"></i><i class="on"></i><i></i></span> High</span>
<span class="k">Cycle</span><span class="v">Cycle 12</span>
<span class="k">Created</span><span class="v" style="font-weight:400;color:var(--fg-4);">May 1, 2026</span>
</div>
<div class="activity-label">Activity</div>
<div class="act">
<div class="act-av orbit">O</div>
<div class="act-text"><strong>Orbit</strong> flagged — 5 days without update <span class="t">· today 06:42</span></div>
</div>
<div class="act">
<div class="act-av">S</div>
<div class="act-text"><strong>Sara</strong> added label <em>auth</em> <span class="t">· May 1</span></div>
</div>
<div class="act">
<div class="act-av">E</div>
<div class="act-text"><strong>Eli</strong> moved to In Progress <span class="t">· May 1</span></div>
</div>
</div>
</div>
<div class="issue-right">
<span class="issue-time">5d</span>
<div class="pri high"><i class="on"></i><i class="on"></i><i class="on"></i><i></i></div>
</div>
</div>
<div class="issue" data-id="1" onclick="toggle(this)">
<div class="issue-status">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="5" fill="var(--status-review)"/>
<circle cx="8" cy="8" r="2" fill="var(--surface)"/>
</svg>
</div>
<div class="issue-body">
<div class="issue-top">
<span class="issue-id">DES-22</span>
<span class="issue-title">Login v2 design</span>
</div>
<div class="issue-sub"><strong>Marie</strong> moved to In Review</div>
<div class="issue-detail">
<p class="detail-desc">Updated login flow with SSO support, passkey prompt, and new branding. 4 screens: email entry, SSO redirect, passkey, and error state.</p>
<div class="props">
<span class="k">Assignee</span><span class="v">Marie</span>
<span class="k">Priority</span><span class="v"><span class="pri"><i class="on"></i><i class="on"></i><i></i><i></i></span> Medium</span>
<span class="k">Cycle</span><span class="v">Cycle 12</span>
<span class="k">Labels</span>
<span class="v" style="gap:4px"><span class="pill"><span class="d" style="background:var(--status-review)"></span>design</span><span class="pill"><span class="d" style="background:var(--status-progress)"></span>login</span></span>
</div>
<div class="activity-label">Activity</div>
<div class="act">
<div class="act-av">M</div>
<div class="act-text"><strong>Marie</strong> moved to In Review <span class="t">· May 5, 18:22</span></div>
</div>
</div>
</div>
<div class="issue-right">
<span class="issue-time">6h</span>
<div class="pri"><i class="on"></i><i class="on"></i><i></i><i></i></div>
</div>
</div>
<div class="issue" data-id="2" onclick="toggle(this)">
<div class="issue-status">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="5.5" stroke="var(--status-backlog)" stroke-width="1.5" stroke-dasharray="2.5 2.5"/>
</svg>
</div>
<div class="issue-body">
<div class="issue-top">
<span class="issue-id">ENG-201</span>
<span class="issue-title">CI flaky test</span>
</div>
<div class="issue-sub">New issue — no assignee yet</div>
<div class="issue-detail">
<p class="detail-desc"><code>test_rate_limit_concurrent</code> fails intermittently on CI (~15% of runs). Likely a race condition in the test fixture teardown.</p>
<div class="props">
<span class="k">Assignee</span><span class="v" style="color:var(--fg-4);font-weight:400">Unassigned</span>
<span class="k">Priority</span><span class="v"><span class="pri"><i class="on"></i><i></i><i></i><i></i></span> Low</span>
<span class="k">Labels</span>
<span class="v" style="gap:4px"><span class="pill"><span class="d" style="background:var(--fg-4)"></span>ci</span><span class="pill"><span class="d" style="background:#d4513a"></span>flaky</span></span>
</div>
<div class="activity-label">Activity</div>
<div class="act">
<div class="act-av"></div>
<div class="act-text"><strong>Linear</strong> issue created <span class="t">· May 5, 21:08</span></div>
</div>
</div>
</div>
<div class="issue-right">
<span class="issue-time">1d</span>
<div class="pri"><i class="on"></i><i></i><i></i><i></i></div>
</div>
</div>
</div>
</div>
<div class="section updates">
<div class="section-header">
<span class="dot"></span> Updated yesterday <span class="count">2</span>
</div>
<div class="issue-list">
<div class="issue" data-id="3" onclick="toggle(this)">
<div class="issue-status">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="5.5" fill="var(--status-done)"/>
<path d="M5.5 8l1.8 1.8 3.2-3.6" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="issue-body">
<div class="issue-top">
<span class="issue-id">ENG-178</span>
<span class="issue-title">API rate limit</span>
</div>
<div class="issue-sub">Marked as <strong>Done</strong></div>
<div class="issue-detail">
<p class="detail-desc">Sliding-window rate limiting on all public API endpoints. 120 req/min per API key, returns <code>429</code> with <code>Retry-After</code> header.</p>
<div class="props">
<span class="k">Assignee</span><span class="v">Eli</span>
<span class="k">Priority</span><span class="v"><span class="pri"><i class="on"></i><i class="on"></i><i></i><i></i></span> Medium</span>
<span class="k">Cycle</span><span class="v">Cycle 12</span>
</div>
<div class="activity-label">Activity</div>
<div class="act">
<div class="act-av">E</div>
<div class="act-text"><strong>Eli</strong> marked as Done <span class="t">· 22:14</span></div>
</div>
</div>
</div>
<div class="issue-right">
<span class="issue-time">22:14</span>
<div class="pri"><i class="on"></i><i class="on"></i><i></i><i></i></div>
</div>
</div>
<div class="issue" data-id="4" onclick="toggle(this)">
<div class="issue-status">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="5.5" stroke="var(--status-progress)" stroke-width="1.8" stroke-dasharray="17.3 17.3" stroke-dashoffset="-8.65" stroke-linecap="round"/>
</svg>
</div>
<div class="issue-body">
<div class="issue-top">
<span class="issue-id">DES-19</span>
<span class="issue-title">Pricing page tokens</span>
</div>
<div class="issue-sub">Status changed to <strong>In Progress</strong></div>
<div class="issue-detail">
<p class="detail-desc">Extract pricing page color and spacing values into design tokens. Align with the new DS token naming convention (<code>--price-*</code> namespace).</p>
<div class="props">
<span class="k">Assignee</span><span class="v">Marie</span>
<span class="k">Priority</span><span class="v"><span class="pri"><i class="on"></i><i class="on"></i><i></i><i></i></span> Medium</span>
<span class="k">Cycle</span><span class="v">Cycle 12</span>
</div>
<div class="activity-label">Activity</div>
<div class="act">
<div class="act-av">M</div>
<div class="act-text"><strong>Marie</strong> changed status to In Progress <span class="t">· 16:40</span></div>
</div>
</div>
</div>
<div class="issue-right">
<span class="issue-time">16:40</span>
<div class="pri"><i class="on"></i><i class="on"></i><i></i><i></i></div>
</div>
</div>
</div>
</div>
<div class="footer">
<div class="mark">
<svg width="14" height="14" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="7" stroke="currentColor" stroke-width="1.2"/>
<circle cx="9" cy="9" r="2" fill="currentColor"/>
</svg>
Open Orbit · auto-generated 06:42
</div>
<span>Linear only</span>
</div>
</div>
<div class="kb">
<span><kbd></kbd><kbd></kbd> navigate</span>
<span><kbd></kbd> expand</span>
<span><kbd>T</kbd> theme</span>
</div>
<script>
function toggle(el) {
const was = el.classList.contains('expanded');
document.querySelectorAll('.issue.expanded').forEach(i => i.classList.remove('expanded'));
if (!was) el.classList.add('expanded');
}
const tog = document.getElementById('themeToggle');
function setTheme(t) {
document.documentElement.setAttribute('data-theme', t);
tog.textContent = t === 'dark' ? '☽' : '☀';
localStorage.setItem('orbit-theme', t);
}
tog.addEventListener('click', () => {
setTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
});
const saved = localStorage.getItem('orbit-theme');
if (saved) setTheme(saved);
document.addEventListener('keydown', e => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.key === 't' || e.key === 'T') { tog.click(); return; }
const all = [...document.querySelectorAll('.issue')];
const cur = all.findIndex(i => i.classList.contains('expanded'));
if (e.key === 'ArrowDown') { e.preventDefault(); toggle(all[cur < all.length - 1 ? cur + 1 : 0]); }
if (e.key === 'ArrowUp') { e.preventDefault(); toggle(all[cur > 0 ? cur - 1 : all.length - 1]); }
if (e.key === 'Enter' && cur >= 0) { e.preventDefault(); toggle(all[cur]); }
});
// Add an "Open in Linear ↗" anchor on every issue row that opens the
// linear.app URL constructed from the issue identifier.
const TEAM = 'nexu';
document.querySelectorAll('.issue').forEach(issue => {
const id = issue.querySelector('.issue-id')?.textContent?.trim();
if (!id) return;
const url = `https://linear.app/${TEAM}/issue/${id}`;
const a = document.createElement('a');
a.href = url;
a.target = '_blank';
a.rel = 'noopener noreferrer';
a.textContent = '↗';
a.title = `Open ${id} in Linear`;
a.style.cssText = 'margin-left:8px;color:var(--accent);text-decoration:none;font-weight:600;font-size:13px;opacity:0.7;';
a.addEventListener('mouseenter', () => a.style.opacity = '1');
a.addEventListener('mouseleave', () => a.style.opacity = '0.7');
a.addEventListener('click', e => e.stopPropagation()); // don't trigger toggle
const right = issue.querySelector('.issue-right');
if (right) right.appendChild(a);
});
</script>
</body>
</html>

View file

@ -0,0 +1,179 @@
---
name: orbit-notion
description: |
Open Orbit briefing skill — selected by the Orbit pipeline when
Notion is the user's only connected connector, or when the user
explicitly scopes their daily digest to Notion. Pulls the past 24
hours of document edits, comments, mentions, and database row changes
from the user's authenticated Notion connection and renders the
digest as a native Notion page (callout / toggle / database table
primitives). This skill should not be triggered manually — it is
invoked by Orbit's daily-digest scheduler against live Notion data.
triggers:
- "notion briefing"
- "notion digest"
- "doc digest"
- "notion 简报"
- "文档摘要"
od:
mode: prototype
platform: desktop
scenario: orbit
featured: 5
preview:
type: html
entry: index.html
design_system:
requires: false
example_prompt: "Generate today's Open Orbit Notion briefing. Notion is my only connected connector — pull yesterday's document edits, comments, @ mentions, and database row changes and render the digest as a native Notion page."
---
# Orbit · Notion Briefing
Single-connector Orbit template scoped to Notion. The briefing renders
*as a real Notion page* — same chrome, same block primitives, same
typography.
## ⚠️ Source-of-truth protocol (read this first)
**Step 1.** Open and read the shipped `example.html` in this folder
before writing any output. That file is the canonical design — your
job is to **reproduce it**, not reinterpret it.
**Step 2.** Mirror the example's structure 1:1:
- Same DOM hierarchy and class names
- Same H2 sections in the same order (文档变更 → 评论 / @ 提及 → 数据库变更)
- Same bullet rows / comment cards / database table columns and rows
- Same callout(s) and toggle block with the same copy
- Same property chips at the top (Type / Owner / Created)
- Same `<script>` block at the end (page-link → notion.so injection)
**Step 3.** You may refresh mock values (doc titles, mentioned people,
edit timestamps) so they read as "today", but you must **not**
invent extra blocks: no extra H2 sections, no extra callouts, no
extra database columns, no extra emoji decorations. If a detail is
not in `example.html`, it does not belong in your output.
The sections below are a **reference for tokens and visual language**
not a license to extend the page.
## ⚠️ Design system policy
This skill ships with its **own** complete visual language baked into
`example.html` (Notion's native page chrome and block system). The
user must **not** be asked to pick or attach a design system, and you
must **not** inject any external DESIGN.md tokens into the output.
- If the active project has a design system attached, **ignore it**.
- If the user supplies brand tokens or a Figma file, **ignore them**.
- Use exclusively the colors / fonts / radii defined in `example.html`.
This is a hard constraint: the briefing must read as a real Notion
page, not as the user's brand.
## Canvas tokens (use these exact values)
```
ink (Notion black): #37352F
text-secondary: #787774
gray bg (block): #F1F1EF
gray border: #E3E2E0
gray light: #F7F6F3
gray cover: #E9E5E0
white surface: #FFFFFF
blue: #2383E2
blue bg: #D3E5EF
blue text: #24548A
green: #4DAB60
green bg: #DBEDDB
green text: #1D6B2D
orange bg: #FADEC9
orange text: #93531D
yellow bg: #FDE68A
callout bg: #F1F1EF
```
Type stack:
- `-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Helvetica, 'Apple Color Emoji', Arial, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol'`
- Page title: 40px bold
- H2: 24px semibold with 1.6em top margin
- Body: 16px / line-height 1.5
- Captions / breadcrumbs: 14px
Notion always uses generous left/right margins; center the content
column at ~720px max width with the rest as `--gray-light` rails.
## Page sections (top to bottom)
1. **Top app bar** — full-width, white, 45px tall.
Left: Notion-style sidebar toggle (`«`), then breadcrumb path
`Open Orbit Daily Briefing May 6`. Breadcrumb separators in
`text-secondary`. Far right: 🔍 search, ⏱ updates, ⚙ share, ⋯.
2. **Faint left sidebar (optional, may render as a 1px hairline rail)**
to imply Notion's workspace sidebar without rendering it in full.
3. **Cover image** — full-width strip ~200px tall, gray cover color
`#E9E5E0`, optional small "Add cover" hint hidden in the corner.
4. **Page header inside content column** — emoji icon (60px) at top,
then page title `早安简报 · 2026 年 5 月 6 日 (Wed)` in 40px bold,
then a row of property chips (gray):
`🗂 Type: Daily Briefing · 👤 Owner: Eli · 📅 Created: 06:42`.
5. **Synopsis paragraph** — one sentence, italic muted:
*"Auto-generated by Open Orbit from yesterday's Notion activity.
12 events across 8 docs and 2 databases."*
6. **H2 section: 📝 文档变更** — list of bullet rows. Each bullet:
`📄 [doc title]` (bold, hover-link blue), then a soft-block child
showing `[author avatar] [author] edited "[snippet of changed text]"`
with `· 8h ago` muted on the right.
7. **H2 section: 💬 评论 & @ 提及** — list of comment cards.
Each card: `gray bg #F1F1EF` rounded 6px, 12px padding;
`[avatar] [author] · in [doc title]`, then comment body in 15px
regular, then a tiny "Reply" link.
Highlight @-mentions with `blue text #24548A` underlined.
8. **Callout block** — required. `gray bg`, 16px padding, rounded 6px,
left side has a 24px emoji (e.g. 🌟 or 💡). Body:
*"Eli, 你昨天还有 3 条评论没回 — 周三例会前看一下?"*
9. **H2 section: 🗄 数据库变更** — render as a Notion database
table view inline.
Columns: `Name | Status | Updated by | Updated`.
Each cell has `gray border` 1px, slight left/right padding,
row height ~38px. Header row uses 12px caps `text-secondary`.
Status column uses **colored tag pills** with the green/blue/orange
bg + text colors above (`Done` green, `In Progress` blue,
`Triage` orange, `Backlog` gray).
10. **Toggle block** — required. Show a `▶ See 4 more changes` collapsed
toggle that, when expanded, would reveal additional rows. Render
it collapsed (just the chevron + label).
11. **Closing callout** — second callout at the bottom acting as a CTA:
`🚀 在 Open Design 里继续处理 →` linked back to the OD project.
## Block formatting rules
- Heading-block hover icon (`+ ⋮⋮`) can be hinted but kept subtle.
- Use the exact Notion bullet glyph (`•`) and indentation (24px).
- Database tags must be Notion's native pill shape: 2-em radius,
6×4 padding, 12px medium weight.
- Avatars: 18px circles with letter + Notion-style soft pastel bg.
## Implementation constraints (paired do / don't)
| Don't | Do |
|---|---|
| Borrow chrome from another connector (Material / Linear rows / GitHub pills) | Stay 100% in Notion's block primitives — H1 / H2 / bullet / callout / toggle / database table |
| Use lorem ipsum | Write real-shaped Notion copy: doc titles like `Q3 OKR`, `Onboarding 文档`, `团队周报`; people like Marie / Bob / Lily; comments like "这一段需要你确认" |
| Mix serif typography in body | Notion is sans only — use the system stack with emoji fallbacks |
| Render avatars as squares | Always circles, 18px with letter + Notion-style soft pastel bg |
| Add shadows or gradients | Flat surfaces only; differentiate blocks with `#E3E2E0` 1px borders or `#F1F1EF` block backgrounds |
| Use loud accent colors outside the Notion palette | Use only the documented Notion blue / green / orange / yellow tag hues |
| Replace Notion's gray callout bg with a solid color | Callouts must use `#F1F1EF` gray bg + 24px emoji on the left |
| Use placeholder doc names like "Document 1" | Use real-shaped Notion titles in CJK or English that read like a real workspace |

View file

@ -0,0 +1,529 @@
<!doctype html>
<html lang="zh-CN">
<head><script>(function(){
function makeStore(){
var data = {};
var api = {
getItem: function(k){ return Object.prototype.hasOwnProperty.call(data, k) ? data[k] : null; },
setItem: function(k, v){ data[k] = String(v); },
removeItem: function(k){ delete data[k]; },
clear: function(){ data = {}; },
key: function(i){ return Object.keys(data)[i] || null; }
};
Object.defineProperty(api, 'length', { get: function(){ return Object.keys(data).length; } });
return api;
}
function tryShim(name){
var works = false;
try { works = !!window[name] && typeof window[name].getItem === 'function'; void window[name].length; }
catch (_) { works = false; }
if (works) return;
try { Object.defineProperty(window, name, { configurable: true, value: makeStore() }); }
catch (_) { try { window[name] = makeStore(); } catch (__) {} }
}
tryShim('localStorage');
tryShim('sessionStorage');
})();</script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>📒 Notion · 昨日 3 条与你相关</title>
<style>
:root {
--notion-black: #37352F;
--notion-gray-text: #787774;
--notion-gray-bg: #F1F1EF;
--notion-gray-border: #E3E2E0;
--notion-gray-light: #F7F6F3;
--notion-gray-cover: #E9E5E0;
--notion-white: #FFFFFF;
--notion-blue: #2383E2;
--notion-blue-bg: #D3E5EF;
--notion-blue-text: #24548A;
--notion-green: #4DAB60;
--notion-green-bg: #DBEDDB;
--notion-green-text: #1D6B2D;
--notion-orange-bg: #FADEC9;
--notion-orange-text: #93531D;
--notion-yellow-bg: #FDE68A;
--notion-callout-bg: #F1F1EF;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Helvetica, 'Apple Color Emoji', Arial, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol';
font-size: 16px;
line-height: 1.5;
color: var(--notion-black);
background: var(--notion-white);
-webkit-font-smoothing: antialiased;
}
/* — pseudo sidebar — */
.page-wrapper {
display: flex;
min-height: 100vh;
}
.sidebar-hint {
width: 4px;
background: var(--notion-gray-border);
flex-shrink: 0;
position: sticky;
top: 0;
height: 100vh;
}
.main-area {
flex: 1;
min-width: 0;
}
/* — breadcrumb — */
.breadcrumb {
padding: 10px 96px;
font-size: 14px;
color: var(--notion-gray-text);
display: flex;
align-items: center;
gap: 4px;
border-bottom: 1px solid var(--notion-gray-border);
background: var(--notion-white);
position: sticky;
top: 0;
z-index: 10;
}
.breadcrumb span { opacity: 0.5; }
.breadcrumb a {
color: var(--notion-gray-text);
text-decoration: none;
}
.breadcrumb a:hover { color: var(--notion-black); }
/* — cover — */
.cover {
height: 220px;
position: relative;
background:
/* vignette */
radial-gradient(ellipse at 50% 50%, transparent 40%, rgba(15,10,5,0.45) 100%),
/* warm golden light from upper-left */
radial-gradient(ellipse at 25% 20%, rgba(180,140,70,0.35) 0%, transparent 60%),
/* deep shadow pocket lower-right */
radial-gradient(ellipse at 80% 85%, rgba(30,15,10,0.5) 0%, transparent 50%),
/* subtle warm highlight center */
radial-gradient(ellipse at 55% 40%, rgba(160,110,60,0.25) 0%, transparent 45%),
/* base: deep Renaissance brown-black */
linear-gradient(160deg, #3a2a1a 0%, #2a1c10 30%, #1e140c 55%, #2c1e14 80%, #3a2818 100%);
background-size: 100% 100%;
overflow: hidden;
}
.cover::before {
content: "";
position: absolute;
inset: 0;
background:
/* craquelure-like texture */
repeating-linear-gradient(
135deg,
transparent 0px, transparent 18px,
rgba(120,90,50,0.04) 18px, rgba(120,90,50,0.04) 19px
),
repeating-linear-gradient(
45deg,
transparent 0px, transparent 24px,
rgba(80,55,30,0.03) 24px, rgba(80,55,30,0.03) 25px
);
pointer-events: none;
}
.cover::after {
content: "";
position: absolute;
inset: 0;
background:
/* gold-leaf accent streak */
linear-gradient(100deg,
transparent 20%,
rgba(195,160,80,0.08) 35%,
rgba(210,175,90,0.12) 42%,
rgba(195,160,80,0.06) 50%,
transparent 65%
);
pointer-events: none;
}
/* — page body — */
.page-content {
max-width: 900px;
padding: 60px 96px 80px;
}
/* — title — */
.page-title {
font-size: 40px;
font-weight: 700;
line-height: 1.2;
letter-spacing: -0.02em;
margin-bottom: 4px;
color: var(--notion-black);
}
.page-subtitle {
font-size: 14px;
color: var(--notion-gray-text);
margin-bottom: 32px;
}
/* — divider — */
.notion-divider {
border: none;
border-top: 1px solid var(--notion-gray-border);
margin: 24px 0;
}
/* — headings — */
.notion-h2 {
font-size: 24px;
font-weight: 600;
line-height: 1.3;
margin: 32px 0 8px;
color: var(--notion-black);
}
.notion-h2 .emoji-anchor {
margin-right: 6px;
}
/* — bullet list — */
.notion-bullet-list {
list-style: none;
padding-left: 2px;
}
.notion-bullet-list li {
position: relative;
padding: 3px 0 3px 24px;
font-size: 16px;
line-height: 1.65;
}
.notion-bullet-list li::before {
content: "•";
position: absolute;
left: 6px;
color: var(--notion-black);
}
.notion-bullet-list .page-link {
text-decoration: underline;
text-decoration-color: var(--notion-gray-border);
text-underline-offset: 2px;
cursor: pointer;
}
.notion-bullet-list .page-link:hover {
background: var(--notion-gray-bg);
border-radius: 3px;
}
.notion-bullet-list .meta {
color: var(--notion-gray-text);
font-size: 14px;
margin-left: 4px;
}
.notion-bullet-list .person {
color: var(--notion-black);
font-weight: 500;
}
/* — callout — */
.notion-callout {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px 16px 16px 12px;
background: var(--notion-callout-bg);
border-radius: 4px;
margin: 16px 0;
font-size: 16px;
line-height: 1.6;
}
.notion-callout .callout-icon {
font-size: 20px;
flex-shrink: 0;
line-height: 1.4;
}
/* — toggle — */
.notion-toggle {
margin: 12px 0;
}
.notion-toggle summary {
cursor: pointer;
font-size: 16px;
line-height: 1.65;
padding: 3px 0 3px 4px;
list-style: none;
display: flex;
align-items: center;
gap: 4px;
user-select: none;
border-radius: 3px;
}
.notion-toggle summary:hover {
background: var(--notion-gray-bg);
}
.notion-toggle summary::-webkit-details-marker { display: none; }
.notion-toggle summary::before {
content: "▶";
font-size: 10px;
color: var(--notion-gray-text);
transition: transform 0.15s;
flex-shrink: 0;
width: 20px;
text-align: center;
}
.notion-toggle[open] summary::before {
transform: rotate(90deg);
}
.notion-toggle .toggle-content {
padding: 4px 0 4px 26px;
color: var(--notion-gray-text);
font-size: 15px;
}
/* — table (database view) — */
.notion-table-wrap {
margin: 12px 0 24px;
border: 1px solid var(--notion-gray-border);
border-radius: 4px;
overflow: hidden;
}
.notion-table-header {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
font-size: 14px;
font-weight: 500;
color: var(--notion-black);
border-bottom: 1px solid var(--notion-gray-border);
background: var(--notion-white);
}
.notion-table-header .db-icon { font-size: 14px; }
.notion-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.notion-table th {
text-align: left;
padding: 6px 12px;
font-weight: 400;
color: var(--notion-gray-text);
border-bottom: 1px solid var(--notion-gray-border);
background: var(--notion-gray-light);
font-size: 12px;
}
.notion-table td {
padding: 8px 12px;
border-bottom: 1px solid var(--notion-gray-border);
vertical-align: middle;
}
.notion-table tr:last-child td {
border-bottom: none;
}
.notion-table .cell-title {
font-weight: 500;
color: var(--notion-black);
}
.notion-table .cell-title .open-icon {
font-size: 12px;
color: var(--notion-gray-text);
margin-left: 4px;
opacity: 0;
transition: opacity 0.15s;
}
.notion-table tr:hover .cell-title .open-icon {
opacity: 1;
}
/* — tags — */
.tag {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: 500;
line-height: 1.4;
white-space: nowrap;
}
.tag-blue {
background: var(--notion-blue-bg);
color: var(--notion-blue-text);
}
.tag-green {
background: var(--notion-green-bg);
color: var(--notion-green-text);
}
/* — at mention inline — */
.mention {
background: rgba(35, 131, 226, 0.1);
color: var(--notion-blue);
padding: 1px 4px;
border-radius: 3px;
font-weight: 500;
cursor: pointer;
}
/* — responsive — */
@media (max-width: 768px) {
.breadcrumb, .page-content { padding-left: 24px; padding-right: 24px; }
.page-title { font-size: 30px; }
.notion-table-wrap { overflow-x: auto; }
}
</style>
</head>
<body>
<div class="page-wrapper">
<!-- pseudo sidebar -->
<div class="sidebar-hint"></div>
<div class="main-area">
<!-- breadcrumb -->
<div class="breadcrumb">
<a href="#">Open Orbit</a>
<span>/</span>
<a href="#">早安简报</a>
<span>/</span>
<span style="color: var(--notion-black);">5 月 6 日</span>
</div>
<!-- cover -->
<div class="cover">
</div>
<!-- page content -->
<div class="page-content" data-od-id="page-body">
<h1 class="page-title" data-od-id="headline">📒 Notion · 昨日 3 条与你相关</h1>
<p class="page-subtitle">2026-05-06 早安Eli · 由 Open Orbit 自动生成</p>
<hr class="notion-divider" />
<!-- callout -->
<div class="notion-callout" data-od-id="callout">
<span class="callout-icon">💡</span>
<div>今日简报仅包含 <strong>Notion</strong> 连接器的变更。共 3 条通知,其中 1 条 @ 提到了你。</div>
</div>
<!-- 文档变更 -->
<h2 class="notion-h2"><span class="emoji-anchor">📝</span>文档变更</h2>
<ul class="notion-bullet-list" data-od-id="doc-changes">
<li>
<span class="page-link">《Q3 OKR》</span>
<span class="person">Marie</span> 编辑了 2 段
<span class="meta">· 22:14</span>
</li>
<li>
<span class="page-link">《Onboarding 文档》</span>
<span class="person">Bob</span> 新建
<span class="meta">· 19:08</span>
</li>
</ul>
<hr class="notion-divider" />
<!-- 评论 / @ 提及 -->
<h2 class="notion-h2"><span class="emoji-anchor">💬</span>评论 / @ 提及</h2>
<ul class="notion-bullet-list" data-od-id="mentions">
<li>
<span class="page-link">团队周报</span>
<span class="person">Lily</span> 在「设计进度」段落 <span class="mention">@你</span>
<span class="meta">· 16:30</span>
</li>
<li>
<span class="page-link">Q3 OKR</span>
<span class="person">Bob</span> 留言「这一段需要你确认」
<span class="meta">· 22:18</span>
</li>
</ul>
<hr class="notion-divider" />
<!-- 数据库变更 -->
<h2 class="notion-h2"><span class="emoji-anchor">🗄️</span>数据库变更</h2>
<div class="notion-table-wrap" data-od-id="db-table">
<div class="notion-table-header">
<span class="db-icon">📊</span> 项目追踪
</div>
<table class="notion-table">
<thead>
<tr>
<th style="width: 40%;">名称</th>
<th style="width: 20%;">状态</th>
<th style="width: 20%;">更新人</th>
<th style="width: 20%;">时间</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span class="cell-title">Login v2 设计追踪<span class="open-icon"></span></span>
</td>
<td><span class="tag tag-blue">In Progress</span></td>
<td>Marie</td>
<td style="color: var(--notion-gray-text);">21:00</td>
</tr>
<tr>
<td>
<span class="cell-title">API 文档 v2<span class="open-icon"></span></span>
</td>
<td><span class="tag tag-green">Done</span></td>
<td>Bob</td>
<td style="color: var(--notion-gray-text);">17:45</td>
</tr>
</tbody>
</table>
</div>
<!-- toggle -->
<details class="notion-toggle">
<summary>📎 查看原始变更记录</summary>
<div class="toggle-content">
共 5 条 Notion API 事件,已折叠。点击上方展开查看完整日志。
</div>
</details>
</div>
</div>
</div>
<script>
// Make every Notion page-link / database row open the matching page on
// notion.so. Uses a slug derived from the visible title.
function slugify(s) {
return (s || '').replace(/《|》/g, '').trim().replace(/\s+/g, '-').replace(/[^\p{L}\p{N}-]/gu, '');
}
function notionUrl(title) {
return 'https://www.notion.so/nexu/' + slugify(title);
}
document.querySelectorAll('.page-link').forEach(span => {
const url = notionUrl(span.textContent);
const a = document.createElement('a');
a.href = url;
a.target = '_blank';
a.rel = 'noopener noreferrer';
a.style.cssText = 'color:inherit;text-decoration:none;border-bottom:1px solid var(--notion-gray-border);';
a.addEventListener('mouseenter', () => a.style.borderBottomColor = 'var(--notion-blue)');
a.addEventListener('mouseleave', () => a.style.borderBottomColor = 'var(--notion-gray-border)');
while (span.firstChild) a.appendChild(span.firstChild);
span.appendChild(a);
});
document.querySelectorAll('.notion-table tbody tr').forEach(row => {
const title = row.querySelector('.cell-title')?.childNodes[0]?.textContent;
if (!title) return;
const url = notionUrl(title);
row.style.cursor = 'pointer';
row.addEventListener('click', () => window.open(url, '_blank', 'noopener,noreferrer'));
row.addEventListener('mouseenter', () => row.style.background = 'var(--notion-gray-light)');
row.addEventListener('mouseleave', () => row.style.background = '');
});
</script>
</body>
</html>