Skip to content
Closed
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Try the extension without installing it. All deployments (main + PR previews): *
- `//mention` for context-aware participant mentions
- `//mermaid` to insert diagram templates
- `//now` to insert formatted timestamps
- `//color` to pick and insert hex color codes
- Uses `//` prefix to avoid conflicts with GitHub's native `/` commands
- Easily extensible with new commands

Expand Down
1 change: 1 addition & 0 deletions docs/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This folder contains end-user and developer documentation for each registered sl
- [//mention](mention/README.md) – context-aware mention autocomplete for participants
- [//mermaid](mermaid/README.md) – insert Mermaid diagram templates
- [//now](now/README.md) – insert formatted date and timestamps
- [//color](color/README.md) – open a color picker and insert hex color codes

## Options

Expand Down
16 changes: 16 additions & 0 deletions docs/commands/color/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# //color Command

Open a native color picker and insert a hex color code.

## Usage

1. In any GitHub comment/review textarea, type `//color`
2. Pick a color
3. Press **Insert**

## What gets inserted

The selected color is inserted as an uppercase hex value, for example:

- `#EDEDED`
- `#00FF00`
58 changes: 58 additions & 0 deletions src/content/commands/color/command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { colorCommand, DEFAULT_COLOR, normalizeHexColor } from "./command.ts"

const { insertTextAtCursor } = vi.hoisted(() => ({
insertTextAtCursor: vi.fn(),
}))

vi.mock("../../picker/index.ts", () => ({
insertTextAtCursor,
applyStyles: vi.fn(),
getCardStyles: () => ({}),
getButtonStyles: () => ({}),
getInputStyles: () => ({}),
}))

describe("color command", () => {
beforeEach(() => {
vi.clearAllMocks()
})

it("opens setup panel in preflight", async () => {
const result = await colorCommand.preflight()
expect(result.showSetup).toBe(true)
expect(result.renderSetup).toBeDefined()
})

it("normalizes valid hex colors to uppercase", () => {
expect(normalizeHexColor("#abc123")).toBe("#ABC123")
})

it("falls back to default color for invalid values", () => {
expect(normalizeHexColor("abc")).toBe(DEFAULT_COLOR)
expect(normalizeHexColor("#abc")).toBe(DEFAULT_COLOR)
})

it("inserts selected color from setup panel", async () => {
const result = await colorCommand.preflight()
const body = document.createElement("div")
const onComplete = vi.fn()

result.renderSetup?.(body, onComplete)

const input = body.querySelector('input[type="text"]') as HTMLInputElement | null
const button = body.querySelector("button") as HTMLButtonElement | null
expect(input).toBeTruthy()
expect(button).toBeTruthy()

if (!input || !button) {
throw new Error("setup controls not rendered")
}

input.value = "#00ff00"
button.dispatchEvent(new MouseEvent("click", { bubbles: true }))

expect(insertTextAtCursor).toHaveBeenCalledWith("#00FF00")
expect(onComplete).toHaveBeenCalled()
})
})
101 changes: 101 additions & 0 deletions src/content/commands/color/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* //color slash command implementation
*
* Opens a native color picker and inserts hex color values.
*/

import { registerCommand, type CommandSpec } from "../registry.ts"
import {
insertTextAtCursor,
applyStyles,
getCardStyles,
getButtonStyles,
getInputStyles,
} from "../../picker/index.ts"

const DEFAULT_COLOR = "#EDEDED"

export function normalizeHexColor(value: string): string {
return /^#[0-9a-fA-F]{6}$/.test(value) ? value.toUpperCase() : DEFAULT_COLOR
}

function renderColorSetupPanel(bodyEl: HTMLElement, onComplete: () => void): void {
const wrap = document.createElement("div")
applyStyles(wrap, getCardStyles())
wrap.style.display = "flex"
wrap.style.flexDirection = "column"
wrap.style.gap = "10px"

const title = document.createElement("div")
title.textContent = "Pick a color to insert as a hex code."

const controls = document.createElement("div")
controls.style.display = "flex"
controls.style.gap = "10px"
controls.style.alignItems = "center"

const colorInput = document.createElement("input")
colorInput.type = "color"
colorInput.value = DEFAULT_COLOR.toLowerCase()
colorInput.ariaLabel = "Color picker"
colorInput.style.width = "44px"
colorInput.style.height = "36px"
colorInput.style.padding = "0"
colorInput.style.border = "none"
colorInput.style.background = "transparent"
colorInput.style.cursor = "pointer"

const hexInput = document.createElement("input")
hexInput.type = "text"
hexInput.value = DEFAULT_COLOR
hexInput.placeholder = "#RRGGBB"
hexInput.ariaLabel = "Hex color"
applyStyles(hexInput, getInputStyles())

colorInput.addEventListener("input", () => {
hexInput.value = normalizeHexColor(colorInput.value)
})

const insert = (): void => {
const normalized = normalizeHexColor(hexInput.value)
insertTextAtCursor(normalized)
onComplete()
}

hexInput.addEventListener("keydown", (event: KeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault()
insert()
}
})

const insertButton = document.createElement("button")
insertButton.type = "button"
insertButton.textContent = "Insert"
insertButton.ariaLabel = "Insert color"
applyStyles(insertButton, getButtonStyles())
insertButton.addEventListener("click", insert)

controls.append(colorInput, hexInput, insertButton)
wrap.append(title, controls)
bodyEl.appendChild(wrap)
}

const colorCommand: CommandSpec = {
preflight: async () => ({
showSetup: true,
message: "Pick a color to insert",
renderSetup: renderColorSetupPanel,
}),
getEmptyState: async () => ({ items: [] }),
getResults: async () => ({ items: [] }),
renderItems: () => {},
onSelect: () => {},
}

registerCommand("color", colorCommand, {
icon: "🎨",
description: "Pick and insert hex color codes",
})

export { colorCommand, DEFAULT_COLOR }
5 changes: 5 additions & 0 deletions src/content/commands/color/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Color Command Module
*/

export * from "./command.ts"
1 change: 1 addition & 0 deletions src/content/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export * from "./emoji/index.ts"
export * from "./mermaid/index.ts"
export * from "./mention/index.ts"
export * from "./now/index.ts"
export * from "./color/index.ts"
export * from "./kbd/index.ts"
export * from "./link/index.ts"
1 change: 1 addition & 0 deletions src/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import "./commands/emoji/index.ts"
import "./commands/mermaid/index.ts"
import "./commands/mention/index.ts"
import "./commands/now/index.ts"
import "./commands/color/index.ts"
import "./commands/kbd/index.ts"
import "./commands/link/index.ts"

Expand Down
Loading