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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/admin/plan_types.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ActiveAdmin.register CoPlan::PlanType, as: "PlanType" do
permit_params :name, :description, :icon, :template_content
permit_params :name, :description, :icon, :behavior, :template_content

index do
selectable_column
Expand All @@ -22,6 +22,9 @@
f.input :icon, as: :select,
collection: CoPlan::PlansHelper::PLAN_TYPE_ICONS.keys,
include_blank: "(default document icon)"
f.input :behavior, as: :select,
collection: CoPlan::PlanType::BEHAVIORS,
include_blank: false
f.input :template_content, as: :text
end
f.actions
Expand All @@ -32,6 +35,7 @@
row :id
row :name
row :icon
row :behavior
row :description
row :default_tags
row :template_content
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# This migration comes from co_plan (originally 20260820000000)
class AddBehaviorToCoplanPlanTypes < ActiveRecord::Migration[8.1]
# Behavior is a column rather than a name match: type names are
# host-editable data, and renaming "Slideshow" must not strip a deck of
# its deck rendering.
def change
add_column :coplan_plan_types, :behavior, :string, limit: 20, null: false, default: "document"
end
end
1 change: 1 addition & 0 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 59 additions & 2 deletions db/seeds/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ module DevelopmentSeed
{ name: "Product Brief", icon: "lightbulb", description: "Product context, goals, and measures of success", default_tags: [ "product" ] },
{ name: "Runbook", icon: "wrench", description: "Operational diagnosis and recovery steps", default_tags: [ "operations" ] },
{ name: "Research Note", icon: "flask", description: "Findings, evidence, and open questions", default_tags: [ "research" ] },
{ name: "Roadmap", icon: "map", description: "Sequenced outcomes and milestones", default_tags: [ "roadmap" ] }
{ name: "Roadmap", icon: "map", description: "Sequenced outcomes and milestones", default_tags: [ "roadmap" ] },
{ name: "Presentation", icon: "presentation", behavior: "presentation", description: "A markdown slide deck — `---` starts a new slide", default_tags: [] }
].freeze

DOCUMENTS = [
Expand Down Expand Up @@ -88,6 +89,10 @@ module DevelopmentSeed
{
key: "collab-showcase", author: "priya", type: "Design Doc", title: "Search latency: cutting p95 with a two-tier cache",
tags: %w[search performance caching], visibility: "published", folder: "Engineering/Active projects", fixture: :collab_showcase
},
{
key: "launch-deck", author: "priya", type: "Presentation", title: "Shared workspaces launch — readout deck",
tags: %w[collaboration launch], visibility: "published", folder: "Product/Launches/Shared workspace", fixture: :slideshow_deck
}
].freeze

Expand Down Expand Up @@ -295,6 +300,58 @@ def fetch(key, &compute)
[^p95-baseline]: [Q2 search latency review](https://observability.example.com/d/search-latency) — trailing 30 days: p95 840 ms, p50 118 ms, with fan-out retries accounting for 62% of tail samples.
[^redis-eviction]: [Redis key eviction](https://redis.io/docs/latest/develop/reference/eviction/) — `allkeys-lru` approximates LRU across the whole keyspace, which fits a cache-only tier.
MARKDOWN
# Showcases the slideshow conventions end-to-end: `---` slide breaks,
# speaker-note comments, a visible `***` rule (not a break), checkboxes
# on a later slide (absolute line numbers), and footnote/link-reference
# definitions that live on a different slide than their references.
slideshow_deck: <<~'MARKDOWN',
Q3 launch readout, presented at the product review. A `---` on its own line starts a new slide.

<!-- notes: Open with the one-number summary — adoption doubled. -->

---

## What shipped

- Shared workspaces on web, iOS, and Android
- Folder-level permissions with inherited defaults
- Real-time presence in every document[^presence]

***

Rules like the one above stay visible — only `---` starts a new slide.

---

## Rollout checklist

- [x] Beta cohort (12 teams)
- [x] Pricing page update
- [ ] Follow-up survey to beta admins

<!-- notes: The survey ships Friday; results feed the next readout. -->

---

## How it went

```text
week 1 ████████ 41%
week 2 ██████████████ 72%
week 4 ████████████████ 89%
```

Weekly active teams, per the [launch dashboard][dash].

---

## Ask

Approve headcount for the sync-conflicts workstream.

[dash]: https://observability.example.com/d/workspace-adoption
[^presence]: Presence reuses the comment-notification channel, so it ships with no new infrastructure.
MARKDOWN
spanish: "## Problema\n\nLas personas nuevas necesitan saber qué paso completar.\n\n## Resultado\n\nUna lista breve muestra el siguiente paso.",
japanese: "## 目標\n\n障害の影響を小さくし、復旧までの時間を短縮します。\n\n## 次のステップ\n\n復旧手順を自動で検証します。",
arabic: "## الملخص\n\nتقارن هذه المذكرة بين الجلسات قصيرة العمر وتدوير الرموز.\n\n## الخطوة التالية\n\nتشغيل تجربة محكومة لقياس الأمان."
Expand Down Expand Up @@ -604,7 +661,7 @@ def document_content(definition)
parts = [ "# #{definition.fetch(:title)}" ]

# Fixtures that are complete document bodies — no lorem filler around them.
if %i[spanish japanese arabic code_walkthrough collab_showcase].include?(definition[:fixture])
if %i[spanish japanese arabic code_walkthrough collab_showcase slideshow_deck].include?(definition[:fixture])
parts << fixture
return parts.join("\n\n")
end
Expand Down
60 changes: 60 additions & 0 deletions engine/app/assets/stylesheets/coplan/deck.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/* Deck rendering for presentation-behavior plans.

Provisional review-view styling only: the real slide design system —
layout classifier, type scale steps, themes — arrives with SLIDE_SPEC.md
and replaces most of this file. Two rules are load-bearing already:
everything deck-scoped lives under .deck / .deck-* (so the design system
can be extracted as a standalone stylesheet later), and slide chrome is
structure around the content, never text inside it (comment anchors
count visible-text occurrences). */

.deck {
display: flex;
flex-direction: column;
gap: 28px;
}

.deck-slide {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
aspect-ratio: 16 / 9;
padding: 40px 56px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 12px;
box-shadow: 0 1px 2px rgb(0 0 0 / 0.04);
/* Content that doesn't fit scrolls rather than clips — the visible cue
the fit report will later formalize. */
overflow-y: auto;
}

.deck-slide::after {
content: attr(data-slide);
position: absolute;
right: 16px;
bottom: 10px;
font-size: 12px;
color: var(--color-text-muted);
}

/* A slide reads from across the room, not at document scale. Flat bumps
until the classifier assigns real type-scale steps per layout. */
.deck-slide .markdown-rendered h1 {
font-size: 2.2em;
line-height: 1.15;
border: none;
}

.deck-slide .markdown-rendered h2 {
font-size: 1.6em;
line-height: 1.2;
border: none;
}

.deck-slide .markdown-rendered p,
.deck-slide .markdown-rendered li {
font-size: 1.12em;
line-height: 1.5;
}
1 change: 1 addition & 0 deletions engine/app/controllers/coplan/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def self.controller_path
helper CoPlan::ApplicationHelper
helper CoPlan::PlansHelper
helper CoPlan::MarkdownHelper
helper CoPlan::SlideshowsHelper
helper CoPlan::CommentsHelper
helper CoPlan::ReferencesHelper
helper CoPlan::PlanEventsHelper
Expand Down
16 changes: 11 additions & 5 deletions engine/app/helpers/coplan/markdown_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@ module MarkdownHelper
# one markdown fragment (e.g. each comment) — commonmarker numbers
# footnote ids from #fn-1 per document, so unprefixed fragments collide
# and reference/backref links jump to the wrong footnote.
def render_markdown(content, interactive: true, footnote_prefix: nil, footnotes: :inline)
#
# line_offset: pass the fragment's 0-based starting line in the full
# document when rendering a slice of a larger plan (slideshow slides).
# Checkbox toggles write to source lines by number, so their data-line
# must stay document-absolute even when the render sees only a fragment.
def render_markdown(content, interactive: true, footnote_prefix: nil, footnotes: :inline, line_offset: 0)
render_options = { unsafe: true }
# Sourcepos is only needed to wire checkboxes to their source lines;
# make_checkboxes_interactive strips it from the final output.
Expand All @@ -55,7 +60,7 @@ def render_markdown(content, interactive: true, footnote_prefix: nil, footnotes:
with_chips = transform_mention_anchors(html)
with_references = transform_reference_anchors(with_chips, numbered_sections: footnote_prefix.nil?)
sanitized = sanitize(with_references, tags: ALLOWED_TAGS, attributes: ALLOWED_ATTRIBUTES)
result = interactive ? make_checkboxes_interactive(sanitized, content) : sanitized
result = interactive ? make_checkboxes_interactive(sanitized, content, line_offset: line_offset) : sanitized
result = scope_footnote_ids(result, footnote_prefix) if footnote_prefix
result = select_footnotes(result, footnotes)
return result.html_safe if footnotes == :only
Expand Down Expand Up @@ -188,8 +193,9 @@ def select_footnotes(html, mode)
# sourcepos metadata, so the parser that decides what renders as a
# checkbox is also the authority on which line it came from. A checkbox
# only becomes interactive when its own source line matches
# TASK_LINE_PATTERN.
def make_checkboxes_interactive(html, content)
# TASK_LINE_PATTERN. Sourcepos lines are fragment-relative; line_offset
# shifts the emitted data-line back to document coordinates.
def make_checkboxes_interactive(html, content, line_offset: 0)
doc = Nokogiri::HTML::DocumentFragment.parse(html)
source_lines = content.to_s.each_line.map(&:rstrip)

Expand All @@ -204,7 +210,7 @@ def make_checkboxes_interactive(html, content)
cb.remove_attribute("disabled")
cb["data-action"] = "coplan--checkbox#toggle"
cb["data-line-text"] = line_text
cb["data-line"] = line_number.to_s
cb["data-line"] = (line_number + line_offset).to_s

li.add_class("task-list-item")

Expand Down
3 changes: 2 additions & 1 deletion engine/app/helpers/coplan/plans_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ def hidden_state_flag(label, title)
"map" => %(<path d="M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z"/><path d="M15 5.764v15"/><path d="M9 3.236v15"/>),
"flask" => %(<path d="M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2"/><path d="M8.5 2h7"/><path d="M7 16h10"/>),
"shield" => %(<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/>),
"wrench" => %(<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>)
"wrench" => %(<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>),
"presentation" => %(<path d="M2 3h20"/><path d="M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3"/><path d="m7 21 5-5 5 5"/>)
}.freeze

# How many tint classes exist in CSS (.plan-type-icon--0 … --N-1).
Expand Down
Loading
Loading