Skip to content

Commit

Permalink
feat: add opt-in fix for pseudo-elements
Browse files Browse the repository at this point in the history
* add fix for pseudo elements

* only replace classname if it exists

* use correct variable

* run prettier

* fix example

* hide fix behind a flag

* fix typo
  • Loading branch information
hipstersmoothie authored and chrisvxd committed Dec 3, 2018
1 parent 43556bc commit b1eb6a5
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 20 deletions.
36 changes: 19 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ module.exports = (storybookBaseConfig, configType) => {
newConfig.output.library = "[name]";

return newConfig;
}
};
```

Manually export the `getStorybook` function in your `./config/storybook/config.js` file:
Expand All @@ -67,6 +67,7 @@ story2sketch --url https://localhost:9001/iframe.html --output stories.asketch.j
As stated by [`react-sketchapp`](https://github.com/airbnb/react-sketchapp), it's complicated to manage assets in a design system. Many teams building design systems or component libraries already produce Sketch files for distributing designs and use [Storybook](https://storybook.js.org) to prototype and present the developed components. It can become difficult to keep designs up to date with the latest components, with designers ever playing catchup. `story2sketch` generates a Sketch file from your components via Storybook, so your Sketch designs always stay up to date.

<a name="configuration"><a/>

## Configuration

You can configure `story2sketch` using [the API](#api) via the CLI, configuring your `package.json` or adding a `story2sketch.config.js` file.
Expand All @@ -90,7 +91,6 @@ Add the following to your package.json:
"output": "dist/great-ui.asketch.json"
}
}

```

### story2sketch.config.js
Expand All @@ -101,24 +101,26 @@ Create a file called `story2sketch.config.js` on the root of your project:
module.exports = {
output: "dist/great-ui.asketch.json",
stories: "all"
}
};
```

<a name="api"><a/>

## API

| Parameter | Explanation | Input Type | Default |
|------------------|-----------------------------------------------------------------------------------------------------------------------------------------|---------------|-------------------------------------------------------------------------------------|
| output | Specifies the filename for the generated asketch.json file. | string | `"dist/stories.asketch.json"` |
| input | The location of Storybook's generated iframe.html. Use this over `url` if possible for performance. | string | `"dist/iframe.html"` |
| url | Storybook iframe URL. Will end in `iframe.html`. Prefer `input` for performance if possible. | string | `"http://localhost:9001/iframe.html"` |
| stories | Stories to extract from Storybook. You should probably override the default. | object/string | `"all"` |
| concurrency | Number of headless Chrome tabs to run in parallel. Drastically impacts performance. | integer | `4` |
| symbolGutter | Gutter to place between symbols in Sketch. | integer | `100` |
| viewports | Viewport configuration. Will be arranged left-to-right by width. Try to avoid changing the key, as this is used to identify the symbol. | object | Mobile viewport (320px wide) and desktop viewport (1920px wide). See example below. |
| querySelector | Query selector to select your node on each page. Uses `document.querySelectorAll`. | string | `"#root"` |
| verbose | Verbose logging output. | boolean | `false` |
| puppeteerOptions | Options to be passed directly to `puppeteer.launch`. See [puppeteer docs](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions) for usage. | object | `{}` |
| Parameter | Explanation | Input Type | Default |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | ----------------------------------------------------------------------------------- |
| output | Specifies the filename for the generated asketch.json file. | string | `"dist/stories.asketch.json"` |
| input | The location of Storybook's generated iframe.html. Use this over `url` if possible for performance. | string | `"dist/iframe.html"` |
| url | Storybook iframe URL. Will end in `iframe.html`. Prefer `input` for performance if possible. | string | `"http://localhost:9001/iframe.html"` |
| stories | Stories to extract from Storybook. You should probably override the default. | object/string | `"all"` |
| concurrency | Number of headless Chrome tabs to run in parallel. Drastically impacts performance. | integer | `4` |
| symbolGutter | Gutter to place between symbols in Sketch. | integer | `100` |
| viewports | Viewport configuration. Will be arranged left-to-right by width. Try to avoid changing the key, as this is used to identify the symbol. | object | Mobile viewport (320px wide) and desktop viewport (1920px wide). See example below. |
| querySelector | Query selector to select your node on each page. Uses `document.querySelectorAll`. | string | `"#root"` |
| verbose | Verbose logging output. | boolean | `false` |
| fixPseudo | Attempt to insert real elements in place of pseudo-elements | boolean | `false` |
| puppeteerOptions | Options to be passed directly to `puppeteer.launch`. See [puppeteer docs](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions) for usage. | object | `{}` |

### Example story2sketch.config.js

Expand Down Expand Up @@ -171,11 +173,11 @@ module.exports = {
]
}
]
}
};
```


<a name="questions"><a/>

## Questions

### Why does my stuff look bad?
Expand Down
75 changes: 74 additions & 1 deletion src/browser/page2layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,80 @@ import {
const getNodeName = node =>
node.id || node.className || node.nodeName.toLowerCase();

const fixPseudoElements = () => {
// Hide old pseudo-elements during render
const css =
".before-reset::before, .after-reset::after { content: none !important; }";
const head = document.head || document.getElementsByTagName("head")[0];
const style = document.createElement("style");

style.type = "text/css";
style.appendChild(document.createTextNode(css));
head.appendChild(style);

const allElements = document.querySelectorAll("body *");
const oldFakes = document.querySelectorAll("body .fake-pseudo");

// Remove old fake pseudo-elements to handle multiple screen sizes
if (oldFakes) {
Array.from(oldFakes).map(el => el.remove());
}

for (let i = 0; i < allElements.length; i++) {
// Remove reset so we can get the screen sizes pseudo-element styles
if (
allElements[i].className &&
typeof allElements[i].className === "string"
) {
allElements[i].className = allElements[i].className.replace(
"before-reset",
""
);
allElements[i].className = allElements[i].className.replace(
"after-reset",
""
);
}

const elementBeforeStyles = window.getComputedStyle(
allElements[i],
":before"
);
const elementAfterStyles = window.getComputedStyle(
allElements[i],
":after"
);
const elementBeforeContent = elementBeforeStyles.content;
const elementAfterContent = elementAfterStyles.content;

if (elementBeforeContent && elementBeforeContent !== "none") {
const virtualBefore = document.createElement("span");

virtualBefore.className = "fake-pseudo";
virtualBefore.setAttribute("style", elementBeforeStyles.cssText);
virtualBefore.innerHTML = elementBeforeStyles.content.split('"').join("");
allElements[i].className += " before-reset";
allElements[i].prepend(virtualBefore);
}

if (elementAfterContent && elementAfterContent !== "none") {
const virtualAfter = document.createElement("span");

virtualAfter.className = "fake-pseudo";
virtualAfter.setAttribute("style", elementAfterStyles.cssText);
virtualAfter.innerHTML = elementAfterStyles.content.split('"').join("");
allElements[i].className += " after-reset";
allElements[i].appendChild(virtualAfter);
}
}
};

export const getSymbol = ({
name = "symbol",
x = 0,
y = 0,
querySelector = "#root"
querySelector = "#root",
fixPseudo = false
} = {}) => {
let nodes;

Expand All @@ -25,6 +94,10 @@ export const getSymbol = ({
return null;
}

if (fixPseudo) {
fixPseudoElements();
}

const layer = nodeTreeToSketchGroup(nodes, {
getGroupName: getNodeName,
getRectangleName: getNodeName
Expand Down
5 changes: 4 additions & 1 deletion src/server/Story2sketch.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default class Story2sketch {
symbolGutter = defaultSymbolGutter,
querySelector = "#root",
verbose = false,
fixPseudo = false,
stories,
puppeteerOptions = {}
}) {
Expand All @@ -44,6 +45,7 @@ export default class Story2sketch {
this.querySelector = querySelector;
this.stories = stories;
this.verbose = verbose;
this.fixPseudo = fixPseudo;
this.puppeteerOptions = puppeteerOptions;

// Sort viewports by width
Expand Down Expand Up @@ -195,7 +197,8 @@ export default class Story2sketch {
// Only prefix if symbolPrefix is defined
const params = JSON.stringify({
name: `${symbolPrefix}${name}`,
querySelector: this.querySelector
querySelector: this.querySelector,
fixPseudo: this.fixPseudo
});

// JSON.parse + JSON.stringify hack was originally used until
Expand Down
4 changes: 3 additions & 1 deletion src/server/getStorybook.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ export default async (browser, iframeUrl) => {
waitUntil: "networkidle2"
});

return page.evaluate("(typeof preview === 'object' && typeof preview.getStorybook !== 'undefined') ? preview.getStorybook() : __STORYBOOK_CLIENT_API__.getStorybook()");
return page.evaluate(
"(typeof preview === 'object' && typeof preview.getStorybook !== 'undefined') ? preview.getStorybook() : __STORYBOOK_CLIENT_API__.getStorybook()"
);
};

0 comments on commit b1eb6a5

Please sign in to comment.