diff --git a/SPECGAPS.md b/SPECGAPS.md index e2620ac..78b5f4d 100644 --- a/SPECGAPS.md +++ b/SPECGAPS.md @@ -18,7 +18,7 @@ Gap categories: **SC** = spec correct, fix code | **CS** = code correct, update | A1-6 | ~~On-demand rendering in preview server~~ | preview.allium:53-93 | `Preview.Router` matches post/archive/home/language routes and renders on-demand via `Rendering` | **Resolved:** `Preview.Router` implements on-demand template rendering for post, archive, home, date, tag, category, page, and language-prefixed routes; static file fallback retained for non-HTML assets (pagefind, feeds); 6 tests added | | A1-7 | ~~Template lookup must use all 4 levels (post→tag→category→default)~~ | template_context.allium:267-277 | `resolve_post_template_slug/3` implements tag→category cascade; all callers (preview, generation) updated | **Resolved:** `resolve_post_template_slug/3` in template_selection.ex, callers in preview.ex, router.ex, outputs.ex updated, 8 tests added | | A1-8 | ~~`ValidateLiquid`/`ValidateScript` before publish~~ | template.allium:110, script.allium:165 | `publish_template` validates Liquid via `Liquex.parse`, `publish_script` validates Lua via `BDS.Scripting.validate` | **Resolved:** validation gates added to `publish_template/1` and `publish_script/1`, invalid content returns `{:error, {:invalid_liquid|:invalid_script, reason}}`, 4 tests added | -| A1-9 | 17 preset colors + custom hex in tag picker | editor_tags.allium | Native ``, no preset palette | Fix code: implement preset color palette popover | +| A1-9 | ~~17 preset colors + custom hex in tag picker~~ | editor_tags.allium | `ColourPicker` hook + popover with 17 preset swatches grid and custom hex input, wired to both create and edit forms | **Resolved:** replaced native `` with `ColourPickerPopover` component (17 presets, custom hex #RRGGBB, immediate selection), JS hook for click-away dismiss, 1 test added | | A1-10 | Template file written on create | engine_side_effects.allium:151-153 | Draft templates have `file_path=""` | Fix code: write template file on create | | A1-11 | Graceful shutdown with inflight request tracking | preview.allium:47-48 | Kills acceptor process, no inflight tracking | Fix code: track inflight requests, drain before shutdown | | A1-12 | Real Pagefind integration for search | generation.allium:208 | Stub only: `pagefind-ui.js` is one-liner, `PagefindUI` never defined, search-runtime.js silently bails, client-side search non-functional | Fix code: bundle real Pagefind, build proper fragment index, wire PagefindUI | diff --git a/assets/css/assistant.css b/assets/css/assistant.css index 30355a5..413e0eb 100644 --- a/assets/css/assistant.css +++ b/assets/css/assistant.css @@ -555,3 +555,88 @@ padding: 8px 12px; } } + +/* Colour picker popover */ +.colour-picker-wrap { + position: relative; + display: inline-flex; +} + +.colour-picker-trigger { + width: 28px; + height: 28px; + border-radius: 4px; + border: 1px solid var(--vscode-input-border); + cursor: pointer; + padding: 0; + flex-shrink: 0; +} + +.colour-picker-trigger:hover { + opacity: 0.85; +} + +.colour-picker-popover { + position: absolute; + top: calc(100% + 4px); + left: 0; + z-index: 30; + padding: 8px; + border: 1px solid var(--vscode-dropdown-border, var(--vscode-panel-border)); + border-radius: 6px; + background: var(--vscode-dropdown-background, var(--vscode-sideBar-background)); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); + width: 196px; +} + +.colour-picker-grid { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 4px; +} + +.colour-picker-swatch { + width: 24px; + height: 24px; + border-radius: 4px; + border: 2px solid transparent; + cursor: pointer; + padding: 0; + transition: border-color 0.1s; +} + +.colour-picker-swatch:hover { + border-color: var(--vscode-focusBorder); +} + +.colour-picker-swatch.selected { + border-color: var(--vscode-focusBorder); + box-shadow: 0 0 0 1px var(--vscode-focusBorder); +} + +.colour-picker-custom { + display: flex; + align-items: center; + gap: 6px; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--vscode-panel-border); +} + +.colour-picker-custom label { + font-size: 11px; + color: var(--vscode-descriptionForeground); + white-space: nowrap; +} + +.colour-picker-custom input { + flex: 1; + min-width: 0; + font-size: 12px; + padding: 2px 6px; + border: 1px solid var(--vscode-input-border); + border-radius: 3px; + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); + font-family: monospace; +} diff --git a/assets/js/hooks/colour_picker.js b/assets/js/hooks/colour_picker.js new file mode 100644 index 0000000..905ecf8 --- /dev/null +++ b/assets/js/hooks/colour_picker.js @@ -0,0 +1,45 @@ +export const ColourPicker = { + mounted() { + this._onClickAway = (e) => { + if (!this.el.contains(e.target)) { + this.el.querySelector(".colour-picker-popover")?.classList.add("hidden"); + } + }; + document.addEventListener("mousedown", this._onClickAway); + + this._setupCustomInput(); + }, + + updated() { + this._setupCustomInput(); + }, + + destroyed() { + document.removeEventListener("mousedown", this._onClickAway); + }, + + _setupCustomInput() { + const input = this.el.querySelector(".colour-picker-custom input"); + if (!input || input._cpBound) return; + input._cpBound = true; + + const pushColor = () => { + let val = input.value.trim(); + if (val && !val.startsWith("#")) val = "#" + val; + if (/^#[0-9a-fA-F]{6}$/.test(val)) { + const event = this.el.dataset.pickEvent; + this.pushEventTo(this.el.dataset.target, event, { color: val }); + this.el.querySelector(".colour-picker-popover")?.classList.add("hidden"); + } + }; + + input.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + pushColor(); + } + }); + + input.addEventListener("blur", pushColor); + } +}; diff --git a/assets/js/hooks/index.js b/assets/js/hooks/index.js index cc0daba..58f52ba 100644 --- a/assets/js/hooks/index.js +++ b/assets/js/hooks/index.js @@ -2,6 +2,7 @@ import { AppShell } from "./app_shell.js"; import { SidebarInteractions } from "./sidebar_interactions.js"; import { SettingsSectionScroll, TagsSectionScroll } from "./section_scroll.js"; import { ChatSurface } from "./chat_surface.js"; +import { ColourPicker } from "./colour_picker.js"; import { MenuEditorTree } from "./menu_editor_tree.js"; import { MonacoEditor } from "./monaco_editor.js"; import { MonacoDiffEditor } from "./monaco_diff_editor.js"; @@ -12,6 +13,7 @@ export const Hooks = { SettingsSectionScroll, TagsSectionScroll, ChatSurface, + ColourPicker, MenuEditorTree, MonacoEditor, MonacoDiffEditor diff --git a/lib/bds/desktop/shell_live/tags_editor.ex b/lib/bds/desktop/shell_live/tags_editor.ex index 5c8194b..9e53466 100644 --- a/lib/bds/desktop/shell_live/tags_editor.ex +++ b/lib/bds/desktop/shell_live/tags_editor.ex @@ -16,6 +16,16 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do @tags_sections ~w(cloud manage merge) + @colour_presets ~w( + #ef4444 #f97316 #f59e0b #eab308 #84cc16 + #22c55e #10b981 #14b8a6 #06b6d4 #0ea5e9 + #3b82f6 #6366f1 #8b5cf6 #a855f7 #d946ef + #ec4899 #64748b + ) + + @spec colour_presets() :: [String.t()] + def colour_presets, do: @colour_presets + @spec update(map(), Phoenix.LiveView.Socket.t()) :: {:ok, Phoenix.LiveView.Socket.t()} @impl true def update(%{action: :save} = assigns, socket) do @@ -107,6 +117,26 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do {:noreply, assign(socket, :tags_editor, tags_editor)} end + def handle_event("pick_new_tag_color", %{"color" => color}, socket) do + tags_editor = + Map.put(socket.assigns.tags_editor, :new_tag, %{ + socket.assigns.tags_editor.new_tag + | "color" => color + }) + + {:noreply, assign(socket, :tags_editor, tags_editor)} + end + + def handle_event("pick_edit_tag_color", %{"color" => color}, socket) do + tags_editor = + Map.put(socket.assigns.tags_editor, :edit_draft, %{ + socket.assigns.tags_editor.edit_draft + | "color" => color + }) + + {:noreply, assign(socket, :tags_editor, tags_editor)} + end + def handle_event("save_tag_editor", _params, socket) do {:noreply, do_save(socket)} end @@ -241,6 +271,55 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do end end + attr :color, :string, default: nil + attr :presets, :list, required: true + attr :pick_event, :string, required: true + attr :target, :any, required: true + + defp colour_picker(assigns) do + ~H""" +
+
+
+ + +
+ + + """ + end + + defp normalize_hex(nil), do: nil + defp normalize_hex(""), do: nil + defp normalize_hex(hex), do: String.downcase(hex) + defp load_data(socket) do project_id = socket.assigns.project_id @@ -280,7 +359,8 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do merge_target: Map.get(socket.assigns, :tags_editor, %{}) |> Map.get(:merge_target, List.first(selected) || ""), - selected_section: selected_section + selected_section: selected_section, + colour_presets: @colour_presets } assign(socket, :tags_editor, data) diff --git a/lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex b/lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex index 8c861f3..bc957e4 100644 --- a/lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex +++ b/lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex @@ -38,7 +38,13 @@
- + + <.colour_picker + color={@tags_editor.new_tag["color"]} + presets={@tags_editor.colour_presets} + pick_event="pick_new_tag_color" + target={@myself} + />
@@ -47,7 +53,13 @@
- + + <.colour_picker + color={@tags_editor.edit_draft["color"]} + presets={@tags_editor.colour_presets} + pick_event="pick_edit_tag_color" + target={@myself} + />