diff --git a/app/components/tool-pallete/toolpallete.element.js b/app/components/tool-pallete/toolpallete.element.js index 068ac762..e8e262a9 100644 --- a/app/components/tool-pallete/toolpallete.element.js +++ b/app/components/tool-pallete/toolpallete.element.js @@ -43,9 +43,9 @@ export default class ToolPallete extends HTMLElement { this.$shadow = this.attachShadow({mode: 'open'}) this.$shadow.innerHTML = this.render() - this.selectorEngine = Selectable() - this.colorPicker = ColorPicker(this.$shadow, this.selectorEngine) - provideSelectorEngine(this.selectorEngine) + this._selectorEngine = Selectable() + this.colorPicker = ColorPicker(this.$shadow, this._selectorEngine) + provideSelectorEngine(this._selectorEngine) } connectedCallback() { @@ -62,7 +62,7 @@ export default class ToolPallete extends HTMLElement { disconnectedCallback() { this.deactivate_feature() - this.selectorEngine.disconnect() + this._selectorEngine.disconnect() hotkeys.unbind( Object.keys(this.toolbar_model).reduce((events, key) => events += ',' + key, '')) @@ -135,9 +135,9 @@ export default class ToolPallete extends HTMLElement { } text() { - this.selectorEngine.onSelectedUpdate(EditText) + this._selectorEngine.onSelectedUpdate(EditText) this.deactivate_feature = () => - this.selectorEngine.removeSelectedCallback(EditText) + this._selectorEngine.removeSelectedCallback(EditText) } align() { @@ -154,15 +154,15 @@ export default class ToolPallete extends HTMLElement { hueshift() { let feature = HueShift(this.colorPicker) - this.selectorEngine.onSelectedUpdate(feature.onNodesSelected) + this._selectorEngine.onSelectedUpdate(feature.onNodesSelected) this.deactivate_feature = () => { - this.selectorEngine.removeSelectedCallback(feature.onNodesSelected) + this._selectorEngine.removeSelectedCallback(feature.onNodesSelected) feature.disconnect() } } inspector() { - this.deactivate_feature = MetaTip(this.selectorEngine) + this.deactivate_feature = MetaTip(this._selectorEngine) } accessibility() { @@ -179,9 +179,9 @@ export default class ToolPallete extends HTMLElement { position() { let feature = Position() - this.selectorEngine.onSelectedUpdate(feature.onNodesSelected) + this._selectorEngine.onSelectedUpdate(feature.onNodesSelected) this.deactivate_feature = () => { - this.selectorEngine.removeSelectedCallback(feature.onNodesSelected) + this._selectorEngine.removeSelectedCallback(feature.onNodesSelected) feature.disconnect() } } @@ -189,6 +189,10 @@ export default class ToolPallete extends HTMLElement { get activeTool() { return this.active_tool.dataset.tool } + + get selectorEngine() { + return this._selectorEngine + } } customElements.define('tool-pallete', ToolPallete) \ No newline at end of file diff --git a/extension/manifest.json b/extension/manifest.json index 6afc32a5..5190ac46 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,11 +1,13 @@ { "name": "PixelBug", - "version": "0.1.5", + "version": "0.1.51", "description": "Hacking for Designers", "manifest_version": 2, "icons": { "128": "icons/128.png" }, + "devtools_page": "panel/index.html", "permissions": [ - "activeTab" + "activeTab", + "contextMenus" ], "background": { "persistent": true, diff --git a/extension/pane/pixelbug/index.html b/extension/pane/pixelbug/index.html new file mode 100644 index 00000000..aae0e585 --- /dev/null +++ b/extension/pane/pixelbug/index.html @@ -0,0 +1,13 @@ + + + + + + +

Hello from PixelBug

+ + \ No newline at end of file diff --git a/extension/pane/sizing/index.html b/extension/pane/sizing/index.html new file mode 100644 index 00000000..d7de2b7e --- /dev/null +++ b/extension/pane/sizing/index.html @@ -0,0 +1,13 @@ + + + + + + +

Calculated/authored/natural/responsive

+ + \ No newline at end of file diff --git a/extension/pane/styleguide/index.html b/extension/pane/styleguide/index.html new file mode 100644 index 00000000..30ec3486 --- /dev/null +++ b/extension/pane/styleguide/index.html @@ -0,0 +1,13 @@ + + + + + + +

fonts/colors/spacing/etc

+ + \ No newline at end of file diff --git a/extension/panel/demo.png b/extension/panel/demo.png new file mode 100644 index 00000000..0f9f3ae0 Binary files /dev/null and b/extension/panel/demo.png differ diff --git a/extension/panel/index.html b/extension/panel/index.html new file mode 100644 index 00000000..e7d75eb4 --- /dev/null +++ b/extension/panel/index.html @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/extension/panel/index.js b/extension/panel/index.js new file mode 100644 index 00000000..a2ec9a6c --- /dev/null +++ b/extension/panel/index.js @@ -0,0 +1,20 @@ +// init panels +chrome.devtools.panels.create('Design', null, 'panel/panel.html', design_panel => { + // design_panel.onShown.addListener(e => + // post({action: 'show-toolbar'})) +}) + +chrome.devtools.panels.elements.createSidebarPane('PixelBug', sidebar => { + sidebar.setPage('../pane/pixelbug/index.html') + sidebar.setHeight('8ex') +}) + +chrome.devtools.panels.elements.createSidebarPane('Style Guide', sidebar => { + sidebar.setPage('../pane/styleguide/index.html') + sidebar.setHeight('8ex') +}) + +chrome.devtools.panels.elements.createSidebarPane('Sizing', sidebar => { + sidebar.setPage('../pane/sizing/index.html') + sidebar.setHeight('8ex') +}) \ No newline at end of file diff --git a/extension/panel/panel.bundle.css b/extension/panel/panel.bundle.css new file mode 100644 index 00000000..d3e48bf1 --- /dev/null +++ b/extension/panel/panel.bundle.css @@ -0,0 +1,89 @@ +:root { + --bg: hsl(0,0%,100%); + --bg-hover: hsl(0,0%,95%); + --dot: hsl(0,0%,90%); + --text: hsl(0,0%,10%); +} + +body { + margin: 0; + background-color: hsl(0,0%,100%); + background-color: var(--bg); + color: hsl(0,0%,10%); + color: var(--text); + background-image: -webkit-repeating-radial-gradient( + center center, + rgba(0,0,0,.05), + rgba(0,0,0,.05) 1px, + transparent 1px, + transparent 100% + ); + background-size: 0.5rem 0.5rem +} + +body.dark { + --bg: hsl(0,0%,15%); + --bg-hover: hsl(0,0%,20%); + --dot: hsl(0,0%,25%); + --text: hsl(0,0%,80%); + } + +img { + width: 100%; + height: 100%; + object-fit: cover; +} + +#layers { + background-color: hsl(0,0%,100%); + background-color: var(--bg); + color: hsl(0,0%,10%); + color: var(--text); + padding: 0.5rem; +} + +ol { + padding: 0; + margin: 0; + list-style-type: none +} + +ol > li, + ol li > .layer { + margin-left: 0.8rem; + } + +details > summary:focus { + /*outline-color: hotpink;*/ + outline: none; + } + +details > summary::-webkit-details-marker { + color: hotpink; + } + +details .layer { + padding: 0.5rem 1rem; + border-radius: 1rem +} + +details .layer:hover { + background: hsl(0,0%,95%); + background: var(--bg-hover); + cursor: pointer; + } + +.layer { + display: inline-flex; + align-items: center; +} + +details [icon] { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background-color: hsl(0,0%,90%); + background-color: var(--dot); + display: inline-block; + margin-right: 0.5rem; +} \ No newline at end of file diff --git a/extension/panel/panel.css b/extension/panel/panel.css new file mode 100644 index 00000000..c57a50ad --- /dev/null +++ b/extension/panel/panel.css @@ -0,0 +1,85 @@ +:root { + --bg: hsl(0,0%,100%); + --bg-hover: hsl(0,0%,95%); + --dot: hsl(0,0%,90%); + --text: hsl(0,0%,10%); +} + +body { + margin: 0; + background-color: var(--bg); + color: var(--text); + background-image: -webkit-repeating-radial-gradient( + center center, + rgba(0,0,0,.05), + rgba(0,0,0,.05) 1px, + transparent 1px, + transparent 100% + ); + background-size: 0.5rem 0.5rem; + + &.dark { + --bg: hsl(0,0%,15%); + --bg-hover: hsl(0,0%,20%); + --dot: hsl(0,0%,25%); + --text: hsl(0,0%,80%); + } +} + +img { + width: 100%; + height: 100%; + object-fit: cover; +} + +#layers { + background-color: var(--bg); + color: var(--text); + padding: 0.5rem; +} + +ol { + padding: 0; + margin: 0; + list-style-type: none; + + & > li, + & li > .layer { + margin-left: 0.8rem; + } +} + +details > summary { + &:focus { + /*outline-color: hotpink;*/ + outline: none; + } + + &::-webkit-details-marker { + color: hotpink; + } +} + +details .layer { + padding: 0.5rem 1rem; + border-radius: 1rem; + + &:hover { + background: var(--bg-hover); + cursor: pointer; + } +} + +.layer { + display: inline-flex; + align-items: center; +} + +details [icon] { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background-color: var(--dot); + display: inline-block; + margin-right: 0.5rem; +} \ No newline at end of file diff --git a/extension/panel/panel.html b/extension/panel/panel.html new file mode 100644 index 00000000..ea89c50b --- /dev/null +++ b/extension/panel/panel.html @@ -0,0 +1,12 @@ + + + + + + + +
+ + + + \ No newline at end of file diff --git a/extension/panel/panel.js b/extension/panel/panel.js new file mode 100644 index 00000000..a92a6ad5 --- /dev/null +++ b/extension/panel/panel.js @@ -0,0 +1,54 @@ +import Channel from '../utils/channel.js' + +const channel_name = 'design-panel' +const Pipe = new Channel({ + name: channel_name, + model: { + tabId: chrome.devtools.inspectedWindow.tabId, + src_channel: channel_name, + target_channel: 'design-artboard', + } +}) + +document.body.classList.add( + chrome.devtools.panels.themeName) + +Pipe.port.onMessage.addListener((message, sender) => { + console.log(`${channel_name} recieved port message`, message, sender) + + if (message.action == 'selected') + render_layers(message.payload) +}) + +Pipe.message.onMessage.addListener((request, sender, sendResponse) => { + console.log(`${channel_name} onMessage`, request) +}) + +// todo: render node props then children +const recurse_nodes = ({nodeName, className, id, children}) => ` +
+ + + ${nodeName}${id ? '#' + id : ''} ${className} + +
    + ${children.map(node => + node.children.length + ? `
  1. ${recurse_nodes(node)}
  2. ` + : ` +
  3. + + + ${node.nodeName}${node.id ? '#' + node.id : ''} ${node.className} + +
  4. + ` + ).join('')} +
+
+` + +const render_layers = nodes => { + console.log('show in dom', nodes) + document.getElementById('layers').innerHTML = nodes.map(recurse_nodes).join('') +} \ No newline at end of file diff --git a/extension/pixelbug.js b/extension/pixelbug.js index 94dcdda1..668d3486 100644 --- a/extension/pixelbug.js +++ b/extension/pixelbug.js @@ -1,29 +1,108 @@ -let state = { +// setup ports +const connections = {} + +chrome.runtime.onConnect.addListener(port => { + console.log('pixelbug onConnect', port) + + if (port.name != 'design-panel' && port.name != 'design-artboard') { + console.warn(port) + return + } + + port.onMessage.addListener(message => { + console.log('pixelbug port.onMessage', message, port) + + const tabId = port.sender.tab + ? port.sender.tab.id + : message.tabId + + if (!tabId) return console.log('missing tabId', message, port) + + if (message.data.action == 'register') { + if (!connections[tabId]) + connections[tabId] = {} + + connections[tabId][port.name] = port + console.info('pixelbug register success', connections) + return + } + + if (message.data.action == 'show-toolbar') { + toggleIn({id:tabId}) + } + + if (message.target_channel) { + const conn = connections[tabId][message.target_channel] + conn && conn.postMessage(message.data) + } + else + console.warn('pixelbug missing message target') + }) + + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + console.log('pixelbug runtime.onMessage', request, sender) + + // Messages from content scripts should have sender.tab set. + // The are all relayed to the 'panel' connection. + if (request.target == 'design-artboard' && request.tabId) + chrome.tabs.sendMessage(request.tabId, request) + + return true + }) +}) + + +// add/remove pixelbug +const state = { loaded: {}, injected: {}, } -chrome.browserAction.onClicked.addListener(function(tab) { - if (state.loaded[tab.id] && state.injected[tab.id]) { - chrome.tabs.executeScript(tab.id, { file: 'toolbar/eject.js' }) - state.injected[tab.id] = false +const toggleIn = ({id:tab_id}) => { + + // toggle out: it's currently loaded and injected + if (state.loaded[tab_id] && state.injected[tab_id]) { + chrome.tabs.executeScript(tab_id, { file: 'toolbar/eject.js' }) + state.injected[tab_id] = false } - else if (state.loaded[tab.id] && !state.injected[tab.id]) { - chrome.tabs.executeScript(tab.id, { file: 'toolbar/inject.js' }) - state.injected[tab.id] = true + + // toggle in: it's loaded and needs injected + else if (state.loaded[tab_id] && !state.injected[tab_id]) { + chrome.tabs.executeScript(tab_id, { file: 'toolbar/content.js' }) + state.injected[tab_id] = true } + + // fresh start in tab else { - chrome.tabs.insertCSS(tab.id, { file: 'toolbar/bundle.css' }) - chrome.tabs.executeScript(tab.id, { file: 'web-components.polyfill.js' }) - chrome.tabs.executeScript(tab.id, { file: 'toolbar/bundle.js' }) - chrome.tabs.executeScript(tab.id, { file: 'toolbar/inject.js' }) + chrome.tabs.insertCSS(tab_id, { file: 'toolbar/bundle.css' }) + chrome.tabs.executeScript(tab_id, { file: 'web-components.polyfill.js' }) + chrome.tabs.executeScript(tab_id, { file: 'toolbar/bundle.js' }) + chrome.tabs.executeScript(tab_id, { file: 'toolbar/content.js' }) - state.loaded[tab.id] = true - state.injected[tab.id] = true + state.loaded[tab_id] = true + state.injected[tab_id] = true } chrome.tabs.onUpdated.addListener(function(tabId) { - if (tabId === tab.id) + if (tabId === tab_id) state.loaded[tabId] = false }) -}) \ No newline at end of file +} + +chrome.browserAction.onClicked.addListener(toggleIn) + +chrome.contextMenus.create({ + title: 'Inspect' +}) + +chrome.contextMenus.onClicked.addListener((menuInfo, tab) => { + toggleIn(tab) + + chrome.tabs.query({active: true, currentWindow: true}, function([tab]) { + tab && chrome.tabs.sendMessage(tab.id, { + action: 'toolSelected', + params: 'inspector', + }) + }) +}) + diff --git a/extension/toolbar/content.js b/extension/toolbar/content.js new file mode 100644 index 00000000..9e65d402 --- /dev/null +++ b/extension/toolbar/content.js @@ -0,0 +1,70 @@ +class Channel { + constructor({name, model}) { + this._port = chrome.runtime.connect({ name }); + this._message = chrome.runtime; + this._model = model; + + this.post({action: 'register'}); + } + + post(data) { + this._port.postMessage( + Object.assign(this._model, { data })); + } + + message(data) { + this._message.postMessage( + Object.assign(this._model, { data })); + } + + get port() { + return this._port + } + + get message() { + return this._message + } +} + +var pallete = document.createElement('tool-pallete'); +const channel_name = 'design-artboard'; +const appendPallete = () => document.body.prepend(pallete); + +const Pipe = new Channel({ + name: channel_name, + model: { + src_channel: channel_name, + target_channel: 'design-panel', + } +}); + +const layersFromDOM = ({nodeName, className, id, children}) => ({ + nodeName, className, id, + children: [...children].map(layersFromDOM), +}); + +// append and watch toolbar selections +appendPallete(); +pallete.selectorEngine.onSelectedUpdate(nodes => + Pipe.post({ + action: 'selected', + payload: nodes.map(layersFromDOM), + })); + +// watch pipe messages (they'll be auto filtered for this pipe) +Pipe.port.onMessage.addListener(message => { + console.log(`${channel_name} recieved port message`, message); +}); + +Pipe.message.onMessage.addListener((request, sender, sendResponse) => { + console.log(`${channel_name} onMessage`, request); + + const { action, params } = request; + + // only respond to toolSelection atm + if (action != 'toolSelected') return + + const [pallete] = document.getElementsByTagName('tool-pallete'); + pallete && pallete[action](params); + // todo: send for tool to select element as well +}); diff --git a/extension/toolbar/inject.js b/extension/toolbar/inject.js index 01b341f3..3ef5245b 100644 --- a/extension/toolbar/inject.js +++ b/extension/toolbar/inject.js @@ -1,2 +1,44 @@ -document.body.prepend(document.createElement('hotkey-map')) -document.body.prepend(document.createElement('tool-pallete')) \ No newline at end of file +import Channel from '../utils/channel.js' + +var pallete = document.createElement('tool-pallete') +const channel_name = 'design-artboard' +const appendPallete = () => document.body.prepend(pallete) + +const Pipe = new Channel({ + name: channel_name, + model: { + src_channel: channel_name, + target_channel: 'design-panel', + } +}) + +const layersFromDOM = ({nodeName, className, id, children}) => ({ + nodeName, className, id, + children: [...children].map(layersFromDOM), +}) + +// append and watch toolbar selections +appendPallete() +pallete.selectorEngine.onSelectedUpdate(nodes => + Pipe.post({ + action: 'selected', + payload: nodes.map(layersFromDOM), + })) + +// watch pipe messages (they'll be auto filtered for this pipe) +Pipe.port.onMessage.addListener(message => { + console.log(`${channel_name} recieved port message`, message) +}) + +Pipe.message.onMessage.addListener((request, sender, sendResponse) => { + console.log(`${channel_name} onMessage`, request) + + const { action, params } = request + + // only respond to toolSelection atm + if (action != 'toolSelected') return + + const [pallete] = document.getElementsByTagName('tool-pallete') + pallete && pallete[action](params) + // todo: send for tool to select element as well +}) diff --git a/extension/toolbar/rollup.config.js b/extension/toolbar/rollup.config.js new file mode 100644 index 00000000..7f26c2a1 --- /dev/null +++ b/extension/toolbar/rollup.config.js @@ -0,0 +1,22 @@ +import resolve from 'rollup-plugin-node-resolve' +import postcss from 'rollup-plugin-postcss' + +const path_base = 'extension/toolbar' + +export default { + input: `${path_base}/inject.js`, + output: { + file: `${path_base}/content.js`, + format: 'esm', + sourcemap: false, + }, + plugins: [ + resolve({ + jsnext: true, + }), + postcss({ + extract: false, + inject: false, + }), + ] +} \ No newline at end of file diff --git a/extension/utils/channel.js b/extension/utils/channel.js new file mode 100644 index 00000000..abe71e77 --- /dev/null +++ b/extension/utils/channel.js @@ -0,0 +1,27 @@ +export default class Channel { + constructor({name, model}) { + this._port = chrome.runtime.connect({ name }) + this._message = chrome.runtime + this._model = model + + this.post({action: 'register'}) + } + + post(data) { + this._port.postMessage( + Object.assign(this._model, { data })) + } + + message(data) { + this._message.postMessage( + Object.assign(this._model, { data })) + } + + get port() { + return this._port + } + + get message() { + return this._message + } +} \ No newline at end of file diff --git a/package.json b/package.json index 8cc918f7..236e952d 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,11 @@ "dev:css": "postcss app/index.css -o app/bundle.css -w", "dev:server": "browser-sync start --server 'app' --files 'app/index.html,app/bundle.css,app/bundle.js' --no-open", "extension": "rollup -c && npm run extension:css && npm run extension:copy && npm run extension:zip", - "extension:css": "postcss app/extension.css -o extension/toolbar/bundle.css", + "extension:js": "rollup -c extension/toolbar/rollup.config.js", + "extension:css": "postcss app/extension.css -o extension/toolbar/bundle.css && postcss extension/panel/panel.css -o extension/panel/panel.bundle.css", + "extension:dev": "concurrently \"postcss extension/panel/panel.css -o extension/panel/panel.bundle.css -w\" \"rollup -c extension/toolbar/rollup.config.js -w\"", "extension:copy": "cp app/bundle.js extension/toolbar/", + "extension:build": "npm run extension:js && npm run extension:css", "extension:zip": "mkdir ./pixelbug_v$npm_package_version && cp -R ./extension/* ./pixelbug_v$npm_package_version/ && zip -r ./extension/build/PixelBug_v$npm_package_version.zip ./pixelbug_v$npm_package_version && rm -rf ./pixelbug_v$npm_package_version", "deploy": "gcloud app deploy --project=google.com:atoms-sandbox" },