pinmark 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pinmark
4
+ # Inline CSS used by the engine's UI partials. Kept in Ruby so the partials
5
+ # stay free of large heredoc blocks and the styles can be reused across
6
+ # render contexts without depending on the asset pipeline.
7
+ module Stylesheets
8
+ ACTIVATOR = <<~CSS
9
+ .pinmark-activator {
10
+ position: fixed;
11
+ bottom: 12px;
12
+ left: 12px;
13
+ z-index: 99999;
14
+ padding: 6px 10px;
15
+ background: #1f2937;
16
+ color: #fff;
17
+ border: 1px solid #374151;
18
+ border-radius: 6px;
19
+ font: 12px sans-serif;
20
+ cursor: pointer;
21
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
22
+ }
23
+ .pinmark-activator:hover { background: #374151; }
24
+ .pinmark-activator.is-on { background: #f97316; color: #111; border-color: #f97316; }
25
+ .pinmark-activator.is-on:hover { background: #ea580c; }
26
+ CSS
27
+
28
+ ACTIVATOR_SCRIPT = <<~JS
29
+ (function() {
30
+ var btn = document.getElementById('pinmark-activator');
31
+ if (!btn) return;
32
+ btn.addEventListener('click', function() {
33
+ var on = document.cookie.split('; ').some(function(c) { return c.indexOf('pinmark=1') === 0; });
34
+ if (on) {
35
+ document.cookie = 'pinmark=; path=/; max-age=0';
36
+ } else {
37
+ document.cookie = 'pinmark=1; path=/; max-age=2592000';
38
+ }
39
+ window.location.reload();
40
+ });
41
+ })();
42
+ JS
43
+
44
+ OVERLAY = <<~CSS
45
+ .pinmark-toggle { position: fixed; bottom: 12px; right: 12px; z-index: 99999; padding: 6px 10px; background: #111; color: #fff; border: 0; border-radius: 6px; font: 12px sans-serif; cursor: pointer; }
46
+ .pinmark-toggle.is-active { background: #f97316; color: #111; }
47
+ .pinmark-mode { position: fixed; bottom: 12px; right: 124px; z-index: 99999; padding: 6px 10px; background: #1f2937; color: #fff; border: 0; border-radius: 6px; font: 12px sans-serif; cursor: pointer; }
48
+ .pinmark-mode.is-element { background: #38bdf8; color: #0c1f2e; }
49
+ .pinmark-panel { position: fixed; top: 12px; right: 12px; max-width: 320px; max-height: 70vh; overflow: auto; background: rgba(17,17,17,.92); color: #fff; padding: 12px; border-radius: 8px; font: 12px/1.4 sans-serif; z-index: 99999; display: none; }
50
+ .pinmark-panel.is-open { display: block; }
51
+ .pinmark-highlight { outline: 2px solid #f97316 !important; outline-offset: -2px; cursor: crosshair; }
52
+ .pinmark-highlight-tag { outline: 2px dashed #38bdf8 !important; }
53
+ .pinmark-highlight-context { outline: 2px dotted rgba(249,115,22,.55) !important; outline-offset: -2px; }
54
+ .pinmark-popover { position: fixed; z-index: 99999; background: #111; color: #fff; padding: 8px; border-radius: 6px; min-width: 220px; }
55
+ .pinmark-popover textarea { width: 100%; min-height: 60px; box-sizing: border-box; background: #1f2937; color: #fff; border: 1px solid #374151; border-radius: 4px; padding: 6px; font: 12px/1.4 sans-serif; resize: vertical; }
56
+ .pinmark-popover textarea::placeholder { color: #9ca3af; }
57
+ .pinmark-popover textarea:focus { outline: none; border-color: #38bdf8; }
58
+ .pinmark-popover-actions { display: flex; gap: 6px; margin-top: 6px; }
59
+ .pinmark-popover-actions button { padding: 4px 10px; background: #1f2937; color: #fff; border: 1px solid #374151; border-radius: 4px; font: 12px sans-serif; cursor: pointer; }
60
+ .pinmark-popover-actions button:hover { background: #374151; }
61
+ .pinmark-popover-actions button:first-child { background: #f97316; border-color: #f97316; color: #111; }
62
+ .pinmark-popover-actions button:first-child:hover { background: #ea580c; }
63
+ .pinmark-popover-header { font: 600 12px sans-serif; color: #fff; margin-bottom: 6px; line-height: 1.3; word-break: break-word; }
64
+ .pinmark-popover-header .mcp-source { display: block; font-weight: 400; opacity: .75; font-size: 11px; }
65
+ .pinmark-popover-header .mcp-selector { display: block; font-weight: 400; color: #38bdf8; font-size: 11px; word-break: break-all; }
66
+ .pinmark-popover-debug { margin-top: 6px; color: #ddd; font: 11px sans-serif; }
67
+ .pinmark-popover-debug summary { cursor: pointer; user-select: none; opacity: .7; }
68
+ .pinmark-popover-debug summary:hover { opacity: 1; }
69
+ .pinmark-popover-debug pre { margin: 6px 0 0; padding: 6px; background: rgba(255,255,255,.05); border-radius: 4px; white-space: pre-wrap; word-break: break-word; max-height: 160px; overflow: auto; font: 10px/1.4 ui-monospace, monospace; }
70
+ .pinmark-label { position: fixed; z-index: 99999; padding: 2px 6px; background: #111; color: #fff; font: 11px sans-serif; border-radius: 4px; pointer-events: none; max-width: 360px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
71
+ .pinmark-label.is-tag { background: #38bdf8; color: #0c1f2e; }
72
+ .pinmark-markers { position: absolute; top: 0; left: 0; pointer-events: none; z-index: 99998; }
73
+ .pinmark-marker { position: absolute; pointer-events: auto; transform: translate(-50%, -50%); width: 22px; height: 22px; border-radius: 50%; background: #f97316; color: #111; border: 2px solid #fff; box-shadow: 0 1px 4px rgba(0,0,0,.4); font: 600 11px sans-serif; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; padding: 0; }
74
+ .pinmark-marker:hover { transform: translate(-50%, -50%) scale(1.15); }
75
+ .pinmark-marker.is-resolved { background: #34d399; opacity: .75; }
76
+ .pinmark-marquee { position: absolute; z-index: 99998; border: 1.5px dashed #f97316; background: rgba(249,115,22,.12); pointer-events: none; }
77
+ .pinmark-panel-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 4px; }
78
+ .pinmark-panel-collapse { background: transparent; color: #fff; border: 0; cursor: pointer; padding: 0 6px; font: 14px sans-serif; opacity: .6; }
79
+ .pinmark-panel-collapse:hover { opacity: 1; }
80
+ .pinmark-panel.is-collapsed { max-height: none; padding: 8px 12px; overflow: hidden; }
81
+ CSS
82
+ end
83
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pinmark
4
+ class Tracker
5
+ class StackUnderflowError < StandardError
6
+ end
7
+
8
+ attr_reader :nodes
9
+
10
+ def initialize
11
+ @nodes = []
12
+ @stack = []
13
+ @counter = 0
14
+ end
15
+
16
+ def push(component:, source:)
17
+ @counter += 1
18
+ id = "pm-#{@counter}"
19
+ parent_id = @stack.last
20
+ @nodes << { id:, component:, source:, parent_id: }
21
+ @stack.push(id)
22
+ id
23
+ end
24
+
25
+ def pop
26
+ raise StackUnderflowError, "pinmark stack underflow" if @stack.empty?
27
+
28
+ @stack.pop
29
+ end
30
+
31
+ def tree
32
+ by_parent = @nodes.group_by { |node| node[:parent_id] }
33
+ build = lambda { |parent_id|
34
+ (by_parent[parent_id] || []).map do |node|
35
+ node.merge(children: build.call(node[:id]))
36
+ end
37
+ }
38
+ build.call(nil)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pinmark
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module Pinmark
6
+ module Wrapper
7
+ module_function
8
+
9
+ # Wrap a block's HTML output in pinmark begin/end markers, pushing
10
+ # and popping the per-request tracker. When annotations are inactive (non-dev,
11
+ # or no tracker) the block is invoked verbatim and its return value passes
12
+ # through unchanged.
13
+ #
14
+ # The block must return a String (or something coercible via #to_s).
15
+ def wrap(component:, source:)
16
+ tracker = Pinmark.tracker if Rails.env.development?
17
+ return yield unless tracker
18
+
19
+ id = tracker.push(component:, source:)
20
+ parent_id = tracker.nodes.last[:parent_id]
21
+
22
+ id_attr = escape(id)
23
+ class_attr = escape(component)
24
+ src_attr = escape(source)
25
+ parent_attr = escape(parent_id)
26
+
27
+ begin_marker = %(<!-- pinmark:begin id="#{id_attr}" class="#{class_attr}" src="#{src_attr}" parent="#{parent_attr}" -->)
28
+ end_marker = %(<!-- pinmark:end id="#{id_attr}" -->)
29
+
30
+ content = yield.to_s
31
+ "#{begin_marker}#{content}#{end_marker}".html_safe
32
+ ensure
33
+ tracker&.pop if id
34
+ end
35
+
36
+ def escape(value)
37
+ CGI.escapeHTML(value.to_s).gsub("--", "&#45;&#45;")
38
+ end
39
+ end
40
+ end
data/lib/pinmark.rb ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pinmark/version"
4
+ require "pinmark/engine"
5
+ require "pinmark/stylesheets"
6
+ require "pinmark/tracker"
7
+ require "pinmark/source_locator"
8
+ require "pinmark/wrapper"
9
+ require "pinmark/phlex"
10
+ require "pinmark/hooks/erb_partial"
11
+ require "pinmark/hooks/view_component"
12
+ require "pinmark/mcp/queue"
13
+ require "pinmark/mcp/tools/list_pending"
14
+ require "pinmark/mcp/tools/list_resolved"
15
+ require "pinmark/mcp/tools/mark_addressed"
16
+ require "pinmark/mcp/tools/clear_addressed"
17
+ require "pinmark/mcp/server"
18
+ require "pinmark/mcp/rack_app"
19
+
20
+ module Pinmark
21
+ # Whether the pinmark hooks should be active for the current request.
22
+ # The host app sets Pinmark.tracker (typically per-request via the
23
+ # Session concern) when the dev cookie / param toggles annotations on.
24
+ #
25
+ # We intentionally keep the predicate cheap so the wrapper / hooks can call it
26
+ # on every render in development without measurable overhead.
27
+ def self.active?
28
+ Rails.env.development? && tracker.present?
29
+ end
30
+
31
+ def self.tracker
32
+ return nil unless defined?(::Current) && ::Current.respond_to?(:pinmark)
33
+
34
+ ::Current.pinmark
35
+ end
36
+ end
data/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "pinmark",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Stimulus controller for the pinmark Rails engine. Consumed by webpack-based host apps via a `file:` dependency.",
6
+ "main": "app/javascript/pinmark/pinmark_controller.js",
7
+ "files": [
8
+ "app/javascript/pinmark/pinmark_controller.js"
9
+ ],
10
+ "exports": {
11
+ ".": "./app/javascript/pinmark/pinmark_controller.js",
12
+ "./pinmark_controller": "./app/javascript/pinmark/pinmark_controller.js"
13
+ }
14
+ }
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pinmark
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Przemyslaw Lusar
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: mcp
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.14'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.14'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec-rails
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: phlex-rails
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: view_component
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ description: Pinmark adds a dev-only floating overlay to any Rails app. Click a component
83
+ or any element on the page, leave a comment, and Claude Code consumes the queue
84
+ via MCP — closing the loop between visual feedback and source edits. Works with
85
+ Phlex, ViewComponent, and ERB partials.
86
+ email:
87
+ - lluzak@gmail.com
88
+ executables: []
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - CHANGELOG.md
93
+ - LICENSE.txt
94
+ - README.md
95
+ - app/controllers/concerns/pinmark/session.rb
96
+ - app/controllers/pinmark/annotations_controller.rb
97
+ - app/controllers/pinmark/application_controller.rb
98
+ - app/javascript/pinmark/pinmark_controller.js
99
+ - app/views/pinmark/_activator.html.erb
100
+ - app/views/pinmark/_overlay.html.erb
101
+ - config/importmap.rb
102
+ - config/routes.rb
103
+ - lib/generators/pinmark/install/install_generator.rb
104
+ - lib/pinmark.rb
105
+ - lib/pinmark/engine.rb
106
+ - lib/pinmark/hooks/erb_partial.rb
107
+ - lib/pinmark/hooks/view_component.rb
108
+ - lib/pinmark/mcp/queue.rb
109
+ - lib/pinmark/mcp/rack_app.rb
110
+ - lib/pinmark/mcp/server.rb
111
+ - lib/pinmark/mcp/tools/clear_addressed.rb
112
+ - lib/pinmark/mcp/tools/list_pending.rb
113
+ - lib/pinmark/mcp/tools/list_resolved.rb
114
+ - lib/pinmark/mcp/tools/mark_addressed.rb
115
+ - lib/pinmark/phlex.rb
116
+ - lib/pinmark/source_locator.rb
117
+ - lib/pinmark/stylesheets.rb
118
+ - lib/pinmark/tracker.rb
119
+ - lib/pinmark/version.rb
120
+ - lib/pinmark/wrapper.rb
121
+ - package.json
122
+ homepage: https://github.com/lluzak/pinmark
123
+ licenses:
124
+ - MIT
125
+ metadata:
126
+ homepage_uri: https://github.com/lluzak/pinmark
127
+ source_code_uri: https://github.com/lluzak/pinmark
128
+ changelog_uri: https://github.com/lluzak/pinmark/blob/master/CHANGELOG.md
129
+ rubygems_mfa_required: 'true'
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '3.2'
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubygems_version: 3.7.2
145
+ specification_version: 4
146
+ summary: Pin-style UI annotations that flow into Claude Code via MCP.
147
+ test_files: []