mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-19 17:15:14 +00:00
feat(items): add first-class code metadata for TASK-123 (#41)
This commit is contained in:
+30
-50
@@ -2036,31 +2036,22 @@ func showCmd() *cobra.Command {
|
||||
fmt.Println(item.Content)
|
||||
}
|
||||
|
||||
// Show GitHub PR if linked
|
||||
if item.Fields != "" {
|
||||
var fieldsMap map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(item.Fields), &fieldsMap); err == nil {
|
||||
if prRaw, ok := fieldsMap["github_pr"]; ok {
|
||||
if prMap, ok := prRaw.(map[string]interface{}); ok {
|
||||
fmt.Println("\n--- GitHub PR ---")
|
||||
prNum := ""
|
||||
if n, ok := prMap["number"].(float64); ok {
|
||||
prNum = fmt.Sprintf("#%d", int(n))
|
||||
}
|
||||
prState := fmt.Sprintf("%v", prMap["state"])
|
||||
prURL := fmt.Sprintf("%v", prMap["url"])
|
||||
prTitle := fmt.Sprintf("%v", prMap["title"])
|
||||
|
||||
stateColor := color.New(color.FgGreen)
|
||||
switch prState {
|
||||
case "MERGED":
|
||||
stateColor = color.New(color.FgMagenta)
|
||||
case "CLOSED":
|
||||
stateColor = color.New(color.FgRed)
|
||||
}
|
||||
fmt.Printf("PR %-6s %s %s\n", prNum, stateColor.Sprint(prState), color.New(color.Faint).Sprint(prURL))
|
||||
fmt.Printf(" %q\n", prTitle)
|
||||
}
|
||||
// Show linked code context if present
|
||||
if pr := extractPRFromItem(item); pr != nil {
|
||||
fmt.Println("\n--- GitHub PR ---")
|
||||
prNum := ""
|
||||
if pr.Number > 0 {
|
||||
prNum = fmt.Sprintf("#%d", pr.Number)
|
||||
}
|
||||
stateColor := prStateColor(pr.State)
|
||||
fmt.Printf("PR %-6s %s %s\n", prNum, stateColor.Sprint(pr.State), color.New(color.Faint).Sprint(pr.URL))
|
||||
fmt.Printf(" %q\n", pr.Title)
|
||||
if item.CodeContext != nil {
|
||||
if item.CodeContext.Branch != "" {
|
||||
fmt.Printf("Branch: %s\n", color.New(color.Faint).Sprint(item.CodeContext.Branch))
|
||||
}
|
||||
if item.CodeContext.Repo != "" {
|
||||
fmt.Printf("Repo: %s\n", color.New(color.Faint).Sprint(item.CodeContext.Repo))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4740,7 +4731,7 @@ Examples:
|
||||
}
|
||||
var results []prStatus
|
||||
for _, item := range items {
|
||||
pr := extractPRFromFields(item.Fields)
|
||||
pr := extractPRFromItem(&item)
|
||||
if pr != nil {
|
||||
results = append(results, prStatus{
|
||||
Ref: cli.ItemRef(item),
|
||||
@@ -4760,7 +4751,7 @@ Examples:
|
||||
|
||||
count := 0
|
||||
for _, item := range items {
|
||||
pr := extractPRFromFields(item.Fields)
|
||||
pr := extractPRFromItem(&item)
|
||||
if pr == nil {
|
||||
continue
|
||||
}
|
||||
@@ -4838,7 +4829,7 @@ func githubUnlinkCmd() *cobra.Command {
|
||||
// Helper functions for GitHub integration
|
||||
|
||||
func showItemPRStatus(item *models.Item, bold, dim *color.Color) error {
|
||||
pr := extractPRFromFields(item.Fields)
|
||||
pr := extractPRFromItem(item)
|
||||
if pr == nil {
|
||||
return fmt.Errorf("item %q has no linked PR", item.Slug)
|
||||
}
|
||||
@@ -4862,31 +4853,20 @@ func showItemPRStatus(item *models.Item, bold, dim *color.Color) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractPRFromFields(fieldsJSON string) *GitHubPR {
|
||||
if fieldsJSON == "" || fieldsJSON == "{}" {
|
||||
func extractPRFromItem(item *models.Item) *GitHubPR {
|
||||
if item == nil || item.CodeContext == nil || item.CodeContext.PullRequest == nil {
|
||||
return nil
|
||||
}
|
||||
var fieldsMap map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(fieldsJSON), &fieldsMap); err != nil {
|
||||
return nil
|
||||
pr := item.CodeContext.PullRequest
|
||||
return &GitHubPR{
|
||||
Number: pr.Number,
|
||||
URL: pr.URL,
|
||||
Title: pr.Title,
|
||||
State: pr.State,
|
||||
Branch: item.CodeContext.Branch,
|
||||
Repo: item.CodeContext.Repo,
|
||||
UpdatedAt: pr.UpdatedAt,
|
||||
}
|
||||
prRaw, ok := fieldsMap["github_pr"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
// Re-marshal and unmarshal to properly extract the struct
|
||||
prJSON, err := json.Marshal(prRaw)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var pr GitHubPR
|
||||
if err := json.Unmarshal(prJSON, &pr); err != nil {
|
||||
return nil
|
||||
}
|
||||
if pr.Number == 0 {
|
||||
return nil
|
||||
}
|
||||
return &pr
|
||||
}
|
||||
|
||||
func prStateColor(state string) *color.Color {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
@@ -34,6 +35,7 @@ type Item struct {
|
||||
CollectionIcon string `json:"collection_icon,omitempty"`
|
||||
CollectionPrefix string `json:"collection_prefix,omitempty"`
|
||||
DerivedClosure *ItemDerivedClosure `json:"derived_closure,omitempty"`
|
||||
CodeContext *ItemCodeContext `json:"code_context,omitempty"`
|
||||
}
|
||||
|
||||
// ComputeRef sets the Ref field from CollectionPrefix and ItemNumber.
|
||||
@@ -60,6 +62,77 @@ type ItemDerivedClosure struct {
|
||||
RelatedItems []ItemRelationRef `json:"related_items,omitempty"`
|
||||
}
|
||||
|
||||
type ItemCodeContext struct {
|
||||
Provider string `json:"provider"`
|
||||
Repo string `json:"repo,omitempty"`
|
||||
Branch string `json:"branch,omitempty"`
|
||||
PullRequest *ItemPullRequestMetadata `json:"pull_request,omitempty"`
|
||||
}
|
||||
|
||||
type ItemPullRequestMetadata struct {
|
||||
Number int `json:"number"`
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
State string `json:"state"`
|
||||
UpdatedAt string `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
type githubPRFields struct {
|
||||
Number int `json:"number"`
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
State string `json:"state"`
|
||||
Branch string `json:"branch"`
|
||||
Repo string `json:"repo"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
func ExtractItemCodeContext(fieldsJSON string) *ItemCodeContext {
|
||||
if fieldsJSON == "" || fieldsJSON == "{}" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var fieldsMap map[string]any
|
||||
if err := json.Unmarshal([]byte(fieldsJSON), &fieldsMap); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
raw, ok := fieldsMap["github_pr"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var githubPR githubPRFields
|
||||
if err := json.Unmarshal(payload, &githubPR); err != nil {
|
||||
return nil
|
||||
}
|
||||
if githubPR.Number == 0 && githubPR.URL == "" && githubPR.Branch == "" && githubPR.Repo == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
context := &ItemCodeContext{
|
||||
Provider: "github",
|
||||
Repo: githubPR.Repo,
|
||||
Branch: githubPR.Branch,
|
||||
}
|
||||
if githubPR.Number != 0 || githubPR.URL != "" || githubPR.Title != "" || githubPR.State != "" {
|
||||
context.PullRequest = &ItemPullRequestMetadata{
|
||||
Number: githubPR.Number,
|
||||
URL: githubPR.URL,
|
||||
Title: githubPR.Title,
|
||||
State: githubPR.State,
|
||||
UpdatedAt: githubPR.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
type ItemCreate struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content,omitempty"`
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package models
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestExtractItemCodeContextFromGitHubPRFields(t *testing.T) {
|
||||
context := ExtractItemCodeContext(`{"github_pr":{"number":42,"url":"https://github.com/xarmian/pad/pull/42","title":"Add branch metadata","state":"OPEN","branch":"feat/task-123-branch-pr-metadata","repo":"xarmian/pad","updated_at":"2026-04-02T14:00:00Z"}}`)
|
||||
if context == nil {
|
||||
t.Fatal("expected code context")
|
||||
}
|
||||
if context.Provider != "github" {
|
||||
t.Fatalf("expected provider github, got %q", context.Provider)
|
||||
}
|
||||
if context.Branch != "feat/task-123-branch-pr-metadata" {
|
||||
t.Fatalf("expected branch metadata, got %q", context.Branch)
|
||||
}
|
||||
if context.Repo != "xarmian/pad" {
|
||||
t.Fatalf("expected repo xarmian/pad, got %q", context.Repo)
|
||||
}
|
||||
if context.PullRequest == nil {
|
||||
t.Fatal("expected pull request metadata")
|
||||
}
|
||||
if context.PullRequest.Number != 42 {
|
||||
t.Fatalf("expected PR #42, got #%d", context.PullRequest.Number)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractItemCodeContextReturnsNilForUnrelatedFields(t *testing.T) {
|
||||
if got := ExtractItemCodeContext(`{"status":"open"}`); got != nil {
|
||||
t.Fatal("expected nil code context for unrelated fields")
|
||||
}
|
||||
}
|
||||
@@ -624,6 +624,41 @@ func TestGetItemIncludesDerivedClosureForImplementedItems(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetItemIncludesCodeContext(t *testing.T) {
|
||||
srv := testServer(t)
|
||||
slug := createWSWithCollections(t, srv)
|
||||
|
||||
rr := doRequest(srv, "POST", "/api/v1/workspaces/"+slug+"/collections/tasks/items", map[string]interface{}{
|
||||
"title": "Linked Task",
|
||||
"fields": `{"status":"open"}`,
|
||||
})
|
||||
var item models.Item
|
||||
parseJSON(t, rr, &item)
|
||||
|
||||
fields := `{"status":"open","github_pr":{"number":40,"url":"https://github.com/xarmian/pad/pull/40","title":"Surface lineage relationships and derived closure for TASK-122","state":"MERGED","branch":"feat/task-122-lineage-display","repo":"xarmian/pad","updated_at":"2026-04-02T14:46:09Z"}}`
|
||||
updated, err := srv.store.UpdateItem(item.ID, models.ItemUpdate{Fields: &fields})
|
||||
if err != nil {
|
||||
t.Fatalf("update item fields: %v", err)
|
||||
}
|
||||
|
||||
rr = doRequest(srv, "GET", "/api/v1/workspaces/"+slug+"/items/"+updated.Slug, nil)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("get item: expected 200, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
|
||||
var fetched models.Item
|
||||
parseJSON(t, rr, &fetched)
|
||||
if fetched.CodeContext == nil {
|
||||
t.Fatal("expected code context in item response")
|
||||
}
|
||||
if fetched.CodeContext.Branch != "feat/task-122-lineage-display" {
|
||||
t.Fatalf("expected branch metadata, got %q", fetched.CodeContext.Branch)
|
||||
}
|
||||
if fetched.CodeContext.PullRequest == nil || fetched.CodeContext.PullRequest.Number != 40 {
|
||||
t.Fatalf("expected PR metadata, got %#v", fetched.CodeContext.PullRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemVersions(t *testing.T) {
|
||||
srv := testServer(t)
|
||||
slug := createWSWithCollections(t, srv)
|
||||
|
||||
+12
-4
@@ -122,7 +122,7 @@ func (s *Store) GetItem(id string) (*models.Item, error) {
|
||||
item.CreatedAt = parseTime(createdAt)
|
||||
item.UpdatedAt = parseTime(updatedAt)
|
||||
item.DeletedAt = parseTimePtr(deletedAt)
|
||||
item.ComputeRef()
|
||||
hydrateItemComputedMetadata(&item)
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ func (s *Store) ResolveItemIncludeDeleted(workspaceID, slugOrRef string) (*model
|
||||
item.CreatedAt = parseTime(createdAt)
|
||||
item.UpdatedAt = parseTime(updatedAt)
|
||||
item.DeletedAt = parseTimePtr(deletedAt)
|
||||
item.ComputeRef()
|
||||
hydrateItemComputedMetadata(&item)
|
||||
return &item, nil
|
||||
}
|
||||
if err != sql.ErrNoRows {
|
||||
@@ -302,7 +302,7 @@ func (s *Store) GetItemBySlugIncludeDeleted(workspaceID, slug string) (*models.I
|
||||
item.CreatedAt = parseTime(createdAt)
|
||||
item.UpdatedAt = parseTime(updatedAt)
|
||||
item.DeletedAt = parseTimePtr(deletedAt)
|
||||
item.ComputeRef()
|
||||
hydrateItemComputedMetadata(&item)
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
@@ -1036,8 +1036,16 @@ func scanItems(rows *sql.Rows) ([]models.Item, error) {
|
||||
item.Pinned = pinned == 1
|
||||
item.CreatedAt = parseTime(createdAt)
|
||||
item.UpdatedAt = parseTime(updatedAt)
|
||||
item.ComputeRef()
|
||||
hydrateItemComputedMetadata(&item)
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func hydrateItemComputedMetadata(item *models.Item) {
|
||||
if item == nil {
|
||||
return
|
||||
}
|
||||
item.ComputeRef()
|
||||
item.CodeContext = models.ExtractItemCodeContext(item.Fields)
|
||||
}
|
||||
|
||||
@@ -312,6 +312,62 @@ func TestItemCRUD(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemCodeContextIsHydratedOnRead(t *testing.T) {
|
||||
s := testStore(t)
|
||||
ws := createTestWorkspace(t, s, "Test")
|
||||
col := createTestCollection(t, s, ws.ID, "Tasks")
|
||||
|
||||
item, err := s.CreateItem(ws.ID, col.ID, models.ItemCreate{
|
||||
Title: "Link PR",
|
||||
Fields: `{"status":"open","github_pr":{"number":7,"url":"https://github.com/xarmian/pad/pull/7","title":"Link PR","state":"OPEN","branch":"feat/link-pr","repo":"xarmian/pad","updated_at":"2026-04-02T14:00:00Z"}}`,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateItem error: %v", err)
|
||||
}
|
||||
|
||||
got, err := s.GetItem(item.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetItem error: %v", err)
|
||||
}
|
||||
if got == nil || got.CodeContext == nil {
|
||||
t.Fatal("expected code context on item read")
|
||||
}
|
||||
if got.CodeContext.Branch != "feat/link-pr" {
|
||||
t.Fatalf("expected branch feat/link-pr, got %q", got.CodeContext.Branch)
|
||||
}
|
||||
if got.CodeContext.PullRequest == nil || got.CodeContext.PullRequest.Number != 7 {
|
||||
t.Fatalf("expected PR #7, got %#v", got.CodeContext.PullRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListItemsIncludesCodeContext(t *testing.T) {
|
||||
s := testStore(t)
|
||||
ws := createTestWorkspace(t, s, "Test")
|
||||
col := createTestCollection(t, s, ws.ID, "Tasks")
|
||||
|
||||
_, err := s.CreateItem(ws.ID, col.ID, models.ItemCreate{
|
||||
Title: "Linked Item",
|
||||
Fields: `{"status":"open","github_pr":{"number":9,"url":"https://github.com/xarmian/pad/pull/9","title":"Linked Item","state":"MERGED","branch":"feat/linked-item","repo":"xarmian/pad","updated_at":"2026-04-02T14:10:00Z"}}`,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateItem error: %v", err)
|
||||
}
|
||||
|
||||
items, err := s.ListItems(ws.ID, models.ItemListParams{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListItems error: %v", err)
|
||||
}
|
||||
if len(items) != 1 {
|
||||
t.Fatalf("expected 1 item, got %d", len(items))
|
||||
}
|
||||
if items[0].CodeContext == nil {
|
||||
t.Fatal("expected list items to include code context")
|
||||
}
|
||||
if items[0].CodeContext.PullRequest == nil || items[0].CodeContext.PullRequest.State != "MERGED" {
|
||||
t.Fatalf("expected merged PR metadata, got %#v", items[0].CodeContext.PullRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemListByCollection(t *testing.T) {
|
||||
s := testStore(t)
|
||||
ws := createTestWorkspace(t, s, "Test")
|
||||
|
||||
@@ -69,7 +69,7 @@ func (s *Store) Search(params SearchParams) ([]SearchResult, error) {
|
||||
r.Item.Pinned = pinned == 1
|
||||
r.Item.CreatedAt = parseTime(createdAt)
|
||||
r.Item.UpdatedAt = parseTime(updatedAt)
|
||||
r.Item.ComputeRef()
|
||||
hydrateItemComputedMetadata(&r.Item)
|
||||
// Don't include full content in search results
|
||||
r.Item.Content = ""
|
||||
results = append(results, r)
|
||||
|
||||
@@ -151,6 +151,21 @@ export interface ItemDerivedClosure {
|
||||
related_items?: ItemRelationRef[];
|
||||
}
|
||||
|
||||
export interface ItemPullRequestMetadata {
|
||||
number: number;
|
||||
url: string;
|
||||
title: string;
|
||||
state: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface ItemCodeContext {
|
||||
provider: string;
|
||||
repo?: string;
|
||||
branch?: string;
|
||||
pull_request?: ItemPullRequestMetadata;
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
@@ -174,6 +189,7 @@ export interface Item {
|
||||
collection_prefix?: string;
|
||||
item_number?: number;
|
||||
derived_closure?: ItemDerivedClosure;
|
||||
code_context?: ItemCodeContext;
|
||||
}
|
||||
|
||||
export interface ItemCreate {
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
let itemLinks = $state<ItemLink[]>([]);
|
||||
let relationshipGroups = $derived(item ? buildRelationshipGroups(item, itemLinks) : []);
|
||||
let closureEntries = $derived(item?.derived_closure?.related_items?.map((related) => relationRefEntry(related)) ?? []);
|
||||
let codeContext = $derived(item?.code_context ?? null);
|
||||
|
||||
$effect(() => {
|
||||
if (wsSlug && collSlug && itemSlug) {
|
||||
@@ -494,6 +495,36 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if codeContext}
|
||||
<div class="code-context-section">
|
||||
<h3 class="section-title">Code Context</h3>
|
||||
<div class="code-context-card">
|
||||
<div class="code-context-meta">
|
||||
<span class="code-provider">{formatFieldDisplay(codeContext.provider)}</span>
|
||||
{#if codeContext.repo}
|
||||
<span class="code-chip">{codeContext.repo}</span>
|
||||
{/if}
|
||||
{#if codeContext.branch}
|
||||
<span class="code-chip">{codeContext.branch}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if codeContext.pull_request}
|
||||
<div class="code-pr-row">
|
||||
<a href={codeContext.pull_request.url} class="code-pr-link" target="_blank" rel="noreferrer">
|
||||
PR #{codeContext.pull_request.number}: {codeContext.pull_request.title}
|
||||
</a>
|
||||
<span class="code-pr-state">{formatFieldDisplay(codeContext.pull_request.state)}</span>
|
||||
</div>
|
||||
{#if codeContext.pull_request.updated_at}
|
||||
<div class="code-pr-updated">
|
||||
Updated {relativeTime(codeContext.pull_request.updated_at)}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Layout wrapper -->
|
||||
<div class="item-body layout-{layout}">
|
||||
<!-- Fields -->
|
||||
@@ -904,6 +935,69 @@
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Code context */
|
||||
.code-context-section {
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
.code-context-card {
|
||||
padding: var(--space-4);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.code-context-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
}
|
||||
.code-provider {
|
||||
font-size: 0.8em;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
.code-chip {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8em;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-tertiary);
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.code-pr-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.code-pr-link {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
.code-pr-link:hover {
|
||||
color: var(--accent-blue);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.code-pr-state {
|
||||
font-size: 0.75em;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-tertiary);
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.code-pr-updated {
|
||||
font-size: 0.8em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Derived closure */
|
||||
.closure-notice {
|
||||
margin-top: var(--space-6);
|
||||
|
||||
Reference in New Issue
Block a user