mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
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:
parent
80416b185a
commit
3298cb3756
32 changed files with 4782 additions and 3 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@ export const ar: Dict = {
|
|||
'examples.modePrototypeMobile': 'نماذج أولية · الجوال',
|
||||
'examples.modeDeck': 'شرائح',
|
||||
'examples.modeDocument': 'مستندات وقوالب',
|
||||
'examples.modeOrbit': 'Orbit',
|
||||
'examples.scenarioGeneral': 'عام',
|
||||
'examples.scenarioEngineering': 'هندسة',
|
||||
'examples.scenarioProduct': 'منتج',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -338,6 +338,7 @@ export const fa: Dict = {
|
|||
'examples.modePrototypeMobile': 'نمونه اولیه · موبایل',
|
||||
'examples.modeDeck': 'اسلایدها',
|
||||
'examples.modeDocument': 'اسناد و قالبها',
|
||||
'examples.modeOrbit': 'Orbit',
|
||||
'examples.scenarioGeneral': 'عمومی',
|
||||
'examples.scenarioEngineering': 'مهندسی',
|
||||
'examples.scenarioProduct': 'محصول',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -280,6 +280,7 @@ export const ja: Dict = {
|
|||
'examples.modePrototypeMobile': 'プロトタイプ · モバイル',
|
||||
'examples.modeDeck': 'スライド',
|
||||
'examples.modeDocument': 'ドキュメント & テンプレート',
|
||||
'examples.modeOrbit': 'Orbit',
|
||||
'examples.scenarioGeneral': '一般',
|
||||
'examples.scenarioEngineering': 'エンジニアリング',
|
||||
'examples.scenarioProduct': 'プロダクト',
|
||||
|
|
|
|||
|
|
@ -327,6 +327,7 @@ export const ko: Dict = {
|
|||
'examples.modePrototypeMobile': '프로토타입 · 모바일',
|
||||
'examples.modeDeck': '슬라이드',
|
||||
'examples.modeDocument': '문서 및 템플릿',
|
||||
'examples.modeOrbit': 'Orbit',
|
||||
'examples.scenarioGeneral': '일반',
|
||||
'examples.scenarioEngineering': '엔지니어링',
|
||||
'examples.scenarioProduct': '제품',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -337,6 +337,7 @@ export const ru: Dict = {
|
|||
'examples.modePrototypeMobile': 'Прототипы · Мобильные',
|
||||
'examples.modeDeck': 'Презентации',
|
||||
'examples.modeDocument': 'Документы и шаблоны',
|
||||
'examples.modeOrbit': 'Orbit',
|
||||
'examples.scenarioGeneral': 'Общее',
|
||||
'examples.scenarioEngineering': 'Инженерия',
|
||||
'examples.scenarioProduct': 'Продукт',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -338,6 +338,7 @@ export const uk: Dict = {
|
|||
'examples.modePrototypeMobile': 'Прототипи · Мобільний',
|
||||
'examples.modeDeck': 'Слайди',
|
||||
'examples.modeDocument': 'Документи та шаблони',
|
||||
'examples.modeOrbit': 'Orbit',
|
||||
'examples.scenarioGeneral': 'Загальне',
|
||||
'examples.scenarioEngineering': 'Інженерія',
|
||||
'examples.scenarioProduct': 'Продукт',
|
||||
|
|
|
|||
|
|
@ -332,6 +332,7 @@ export const zhCN: Dict = {
|
|||
'examples.modePrototypeMobile': '原型 · 移动端',
|
||||
'examples.modeDeck': '幻灯片',
|
||||
'examples.modeDocument': '文档与模板',
|
||||
'examples.modeOrbit': 'Orbit',
|
||||
'examples.scenarioGeneral': '通用',
|
||||
'examples.scenarioEngineering': '工程',
|
||||
'examples.scenarioProduct': '产品',
|
||||
|
|
|
|||
|
|
@ -332,6 +332,7 @@ export const zhTW: Dict = {
|
|||
'examples.modePrototypeMobile': '原型 · 行動版',
|
||||
'examples.modeDeck': '投影片',
|
||||
'examples.modeDocument': '文件與範本',
|
||||
'examples.modeOrbit': 'Orbit',
|
||||
'examples.scenarioGeneral': '通用',
|
||||
'examples.scenarioEngineering': '工程',
|
||||
'examples.scenarioProduct': '產品',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
196
skills/orbit-general/SKILL.md
Normal file
196
skills/orbit-general/SKILL.md
Normal 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 10–16 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 2–3 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) + 1–2 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 |
|
||||
1302
skills/orbit-general/example.html
Normal file
1302
skills/orbit-general/example.html
Normal file
File diff suppressed because it is too large
Load diff
160
skills/orbit-github/SKILL.md
Normal file
160
skills/orbit-github/SKILL.md
Normal 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
|
||||
2–3 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 |
|
||||
770
skills/orbit-github/example.html
Normal file
770
skills/orbit-github/example.html
Normal 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>- <div className="briefing-legacy"></div>
|
||||
<div class="diff-line add"><span class="diff-line-number">43</span>+ <Card variant="orbit" density="compact"></div>
|
||||
<div class="diff-line add"><span class="diff-line-number">44</span>+ <BriefingHeader provider={provider} /></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
163
skills/orbit-gmail/SKILL.md
Normal 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 |
|
||||
643
skills/orbit-gmail/example.html
Normal file
643
skills/orbit-gmail/example.html
Normal 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><orbit@opendesign.local></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>
|
||||
170
skills/orbit-linear/SKILL.md
Normal file
170
skills/orbit-linear/SKILL.md
Normal 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, 18–24px |
|
||||
| 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` |
|
||||
571
skills/orbit-linear/example.html
Normal file
571
skills/orbit-linear/example.html
Normal 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>
|
||||
179
skills/orbit-notion/SKILL.md
Normal file
179
skills/orbit-notion/SKILL.md
Normal 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 |
|
||||
529
skills/orbit-notion/example.html
Normal file
529
skills/orbit-notion/example.html
Normal 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>
|
||||
Loading…
Reference in a new issue