@stevendejong/storybook-stylesheet-toggle 1.0.1 → 1.0.2
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.
- package/README.md +46 -1
- package/dist/manager.js +1 -1
- package/dist/manager.js.map +1 -1
- package/dist/preview.js +1 -1
- package/dist/preview.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
# Storybook Stylesheet Toggle addon
|
|
2
2
|
|
|
3
|
-
The Storybook Stylesheet Toggle addon is a simple and convenient tool for
|
|
3
|
+
The Storybook Stylesheet Toggle addon is a simple and convenient tool for managing stylesheets in your Storybook stories. Switch between configured stylesheets and dynamically add custom ones at runtime via a dropdown menu in the toolbar.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install --save-dev @stevendejong/storybook-stylesheet-toggle
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Add the addon to your `.storybook/main.ts`:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
export default {
|
|
17
|
+
addons: [
|
|
18
|
+
'@stevendejong/storybook-stylesheet-toggle',
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
```
|
|
7
22
|
|
|
8
23
|
## Usage
|
|
9
24
|
|
|
@@ -33,3 +48,33 @@ export default preview;
|
|
|
33
48
|
|
|
34
49
|
## Using the Toggle
|
|
35
50
|
Once you've configured your story, you'll see a toolbar button in Storybook that allows you to select and apply different stylesheets to your components during development.
|
|
51
|
+
|
|
52
|
+
### Adding Custom Stylesheets
|
|
53
|
+
|
|
54
|
+
In addition to configured stylesheets, you can dynamically add custom stylesheets at runtime:
|
|
55
|
+
|
|
56
|
+
1. Click the paint brush toolbar button
|
|
57
|
+
2. Enter a stylesheet URL in the input field at the top:
|
|
58
|
+
- Relative paths: `./theme.css`, `/assets/theme.css`
|
|
59
|
+
- Absolute URLs: `https://example.com/styles/theme.css`
|
|
60
|
+
3. Press Enter or click "Add"
|
|
61
|
+
|
|
62
|
+
**Features:**
|
|
63
|
+
- ✅ Custom stylesheets persist across browser sessions
|
|
64
|
+
- ✅ Delete custom stylesheets with the trash icon
|
|
65
|
+
- ✅ URL validation (must end with `.css`)
|
|
66
|
+
- ✅ Duplicate detection
|
|
67
|
+
- ✅ Auto-naming from URL (e.g., `theme.css`)
|
|
68
|
+
- ✅ Visual separation from configured stylesheets
|
|
69
|
+
|
|
70
|
+
**Example workflow:**
|
|
71
|
+
1. Open Storybook
|
|
72
|
+
2. Click paint brush icon
|
|
73
|
+
3. Add custom stylesheet: `https://cdn.example.com/dark-theme.css`
|
|
74
|
+
4. Stylesheet appears in "Custom Stylesheets" section
|
|
75
|
+
5. Switch between custom and configured stylesheets
|
|
76
|
+
6. Delete custom stylesheets when no longer needed
|
|
77
|
+
|
|
78
|
+
**localStorage Storage:**
|
|
79
|
+
- Selected stylesheet: `localStorage.getItem('stylesheetToggle')`
|
|
80
|
+
- Custom stylesheets: `localStorage.getItem('stylesheetToggle:custom')`
|
package/dist/manager.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import l
|
|
1
|
+
import l,{useState}from'react';import {addons,types,useParameter,useGlobals}from'storybook/manager-api';import {WithTooltip,IconButton,Form,Button}from'storybook/internal/components';import {PaintBrushIcon,TrashIcon}from'@storybook/icons';import {styled}from'storybook/theming';var m="stylesheetToggle",x=`${m}/tool`,a="stylesheetToggle";var v={default:""};var g=`${a}:custom`;function c(){let t=localStorage.getItem(g);if(!t)return [];try{let e=JSON.parse(t);if(!e||typeof e!="object")throw new Error("Invalid format");return Object.values(e).filter(s=>s&&typeof s.id=="string"&&typeof s.name=="string"&&typeof s.url=="string")}catch{return localStorage.removeItem(g),[]}}function C(t,e){let s=`custom:${Date.now()}`,r=E(t),n=c(),o=N(r,n),i={id:s,name:o,url:t},h=[...n,i].reduce((y,S)=>(y[S.id]=S,y),{});return localStorage.setItem(g,JSON.stringify(h)),i}function I(t){let r=c().filter(o=>o.id!==t).reduce((o,i)=>(o[i.id]=i,o),{});localStorage.setItem(g,JSON.stringify(r)),localStorage.getItem(a)===t&&localStorage.setItem(a,"default");}function b(t){if(!t.endsWith(".css"))return false;try{return t.startsWith("http://")||t.startsWith("https://")?(new URL(t),!0):t.startsWith("./")||t.startsWith("/")||!t.includes("://")}catch{return false}}function k(t,e){return e.some(s=>s.url===t)}function E(t){let e=t.split("/");return e[e.length-1]||"Custom Stylesheet"}function N(t,e){let s=e.map(n=>n.name);if(!s.includes(t))return t;let r=2;for(;s.includes(`${t} (${r})`);)r++;return `${t} (${r})`}var w=({onAdd:t})=>{let[e,s]=useState(""),[r,n]=useState(null),o=()=>{if(!e.trim()){n("URL is required");return}if(!b(e)){n("Invalid stylesheet URL (must end with .css)");return}let u=c();if(k(e,u)){n("This stylesheet already exists");return}let h=C(e);s(""),n(null),t(h.id);},i=u=>{u.key==="Enter"&&(u.preventDefault(),o());};return l.createElement("div",{style:{padding:"8px",borderBottom:"1px solid #ddd"}},l.createElement(Form.Field,{label:"Add Custom Stylesheet"},l.createElement("div",{style:{display:"flex",gap:"4px"}},l.createElement(Form.Input,{value:e,onChange:u=>s(u.target.value),onKeyDown:i,placeholder:"https://example.com/theme.css",style:{flex:1}}),l.createElement(Button,{onClick:o,size:"small"},"Add")),r&&l.createElement("div",{style:{color:"red",fontSize:"12px",marginTop:"4px"}},r)))};var _=styled.div({minWidth:280,maxWidth:400,overflow:"hidden",overflowY:"auto",maxHeight:"400px"}),D=styled.div(({theme:t,active:e})=>({fontSize:t.typography.size.s1,color:e?t.color.secondary:t.color.dark,textDecoration:"none",cursor:"pointer",lineHeight:"18px",padding:"7px 10px",display:"flex",alignItems:"center",justifyContent:"space-between",backgroundColor:e?t.background.hoverable:"transparent","&:hover":{background:t.background.hoverable}})),K=styled.button(({theme:t})=>({background:"none",border:"none",cursor:"pointer",padding:"2px 4px",display:"flex",alignItems:"center",opacity:.6,"&:hover":{opacity:1,color:t.color.negative}})),F=styled.div(({theme:t})=>({height:"1px",background:t.appBorderColor,margin:"8px 0"})),O=styled.div(({theme:t})=>({fontSize:"11px",color:t.color.mediumdark,padding:"4px 10px",fontWeight:"bold",textTransform:"uppercase"})),$=styled.span({overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",maxWidth:"200px"}),A=({configuredStylesheets:t,activeStylesheet:e,onSelect:s})=>{let r=c(),n=(o,i)=>{o.stopPropagation(),confirm("Delete this custom stylesheet?")&&(I(i),window.location.reload());};return l.createElement(_,null,l.createElement(w,{onAdd:s}),r.length>0&&l.createElement(l.Fragment,null,l.createElement(O,null,"Custom Stylesheets"),r.map(o=>l.createElement(D,{key:o.id,active:e===o.id,onClick:()=>s(o.id)},l.createElement($,{title:o.url},o.name),l.createElement(K,{onClick:i=>n(i,o.id),"aria-label":"Delete stylesheet"},l.createElement(TrashIcon,{size:12})))),l.createElement(F,null)),l.createElement(O,null,"Configured Stylesheets"),Object.entries(t).map(([o])=>l.createElement(D,{key:o,active:e===o,onClick:()=>s(o)},o)))};var V=({stylesheets:t})=>{let[e]=useGlobals(),s=[true,"true"].includes(e[a]);if(t===null)return null;let r={...v,...t},n=localStorage.getItem(a)||"default";return l.createElement(WithTooltip,{placement:"top",trigger:"click",tooltip:l.createElement(A,{configuredStylesheets:r,activeStylesheet:n,onSelect:i=>{localStorage.setItem(a,i),window.location.reload();}}),closeOnOutsideClick:true},l.createElement(IconButton,{key:x,active:s,title:"Toggle Stylesheet"},l.createElement(PaintBrushIcon,null)))},P=V;addons.register(m,()=>{addons.add(m,{title:"Stylesheet Toggle",type:types.TOOL,render:()=>{let t=useParameter(a,null);return l.createElement(P,{stylesheets:t})}});});//# sourceMappingURL=manager.js.map
|
|
2
2
|
//# sourceMappingURL=manager.js.map
|
package/dist/manager.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/constants.ts","../src/defaults.ts","../src/Tool.tsx","../src/manager.tsx"],"names":["ADDON_ID","TOOL_ID","PARAM_KEY","defaultStylesheets","Tool","stylesheets","globals","useGlobals","isActive","mapping","name","url","active","toggleStylesheet","sheet","items","React","WithTooltip","TooltipLinkList","IconButton","PaintBrushIcon","Tool_default","addons","types","useParameter"],"mappings":"+NAAO,IAAMA,CAAAA,CAAW,mBACXC,CAAAA,CAAU,CAAA,EAAGD,CAAQ,CAAA,KAAA,CAAA,CACrBE,CAAAA,CAAY,kBAAA,CCFlB,IAAMC,CAAAA,CAA8C,CACzD,OAAA,CAAW,EACb,CAAA,CCMA,IAAMC,CAAAA,CAAO,CAAC,CAAE,WAAA,CAAAC,CAAY,CAAA,GAAiC,CAC3D,GAAM,CAACC,CAAO,CAAA,CAAIC,UAAAA,GACZC,CAAAA,CAAW,CAAC,IAAA,CAAM,MAAM,CAAA,CAAE,QAAA,CAASF,CAAAA,CAAQJ,CAAS,CAAC,CAAA,CAE3D,GAAIG,CAAAA,GAAgB,IAAA,CAClB,OAAO,KAGT,IAAMI,CAAAA,CAAmC,EAAC,CAC1C,IAAA,GAAW,CAACC,CAAAA,CAAMC,CAAG,CAAA,GAAK,MAAA,CAAO,OAAA,CAAQR,CAAkB,CAAA,CACzDM,CAAAA,CAAQC,CAAI,CAAA,CAAIC,CAAAA,CAElB,IAAA,GAAW,CAACD,CAAAA,CAAMC,CAAG,CAAA,GAAK,MAAA,CAAO,OAAA,CAAQN,CAAW,CAAA,CAClDI,CAAAA,CAAQC,CAAI,CAAA,CAAIC,EAGlB,IAAMC,CAAAA,CAAS,YAAA,CAAa,OAAA,CAAQV,CAAS,CAAA,CACxCU,CAAAA,EACH,YAAA,CAAa,OAAA,CAAQV,CAAAA,CAAW,SAAS,CAAA,CAG3C,IAAMW,CAAAA,CAAoBC,GAAkB,CAC1C,YAAA,CAAa,OAAA,CAAQZ,CAAAA,CAAWY,CAAK,CAAA,CACrC,MAAA,CAAO,QAAA,CAAS,MAAA,GAClB,CAAA,CAEMC,CAAAA,CAAQ,EAAC,CACf,OAAW,CAACL,CAAAA,CAAMC,CAAG,CAAA,GAAK,MAAA,CAAO,OAAA,CAAQF,CAAO,CAAA,CAC9CM,CAAAA,CAAM,IAAA,CAAK,CACT,EAAA,CAAIL,CAAAA,CACJ,KAAA,CAAOA,EACP,OAAA,CAAS,IAAMG,CAAAA,CAAiBH,CAAI,CAAA,CACpC,MAAA,CAAU,CAACE,CAAAA,EAAUF,CAAAA,GAAS,SAAA,EAAcE,CAAAA,GAAWF,CACzD,CAAC,CAAA,CAGH,OACEM,CAAAA,CAAA,aAAA,CAACC,WAAAA,CAAA,CACC,SAAA,CAAU,KAAA,CACV,OAAA,CAAQ,OAAA,CACR,OAAA,CAASD,CAAAA,CAAA,aAAA,CAACE,eAAAA,CAAA,CAAgB,KAAA,CAAOH,EAAO,CAAA,CACxC,mBAAA,CAAmB,IAAA,CAAA,CAEnBC,CAAAA,CAAA,aAAA,CAACG,UAAAA,CAAA,CACC,GAAA,CAAKlB,CAAAA,CACL,MAAA,CAAQO,CAAAA,CACR,KAAA,CAAM,qBAAA,CAAA,CAENQ,CAAAA,CAAA,cAACI,cAAAA,CAAA,IAAe,CAClB,CACF,CAEJ,CAAA,CAEOC,CAAAA,CAAQjB,CAAAA,CCzDfkB,MAAAA,CAAO,QAAA,CAAStB,CAAAA,CAAU,IAAM,CAC9BsB,MAAAA,CAAO,IAAItB,CAAAA,CAAU,CACnB,KAAA,CAAO,mBAAA,CACP,IAAA,CAAMuB,KAAAA,CAAM,IAAA,CACZ,MAAA,CAAQ,IAAM,CACZ,IAAMlB,CAAAA,CAAcmB,YAAAA,CAClBtB,CAAAA,CACA,IACF,CAAA,CAEA,OAAOc,CAAAA,CAAA,aAAA,CAACK,CAAAA,CAAA,CAAK,WAAA,CAAahB,CAAAA,CAAa,CACzC,CACF,CAAC,EACH,CAAC,CAAA","file":"manager.js","sourcesContent":["export const ADDON_ID = \"stylesheetToggle\";\nexport const TOOL_ID = `${ADDON_ID}/tool`;\nexport const PARAM_KEY = `stylesheetToggle`;\n\n","export const defaultStylesheets: {[key: string]: string} = {\n \"default\": ''\n}\n","import React from \"react\";\nimport { useGlobals } from \"storybook/manager-api\";\nimport { IconButton, TooltipLinkList, WithTooltip } from \"storybook/internal/components\";\nimport { PaintBrushIcon } from \"@storybook/icons\";\nimport { PARAM_KEY, TOOL_ID } from \"./constants\";\nimport { defaultStylesheets } from \"./defaults\";\n\n\nconst Tool = ({ stylesheets }: { [key: string]: string }) => {\n const [globals] = useGlobals();\n const isActive = [true, \"true\"].includes(globals[PARAM_KEY]);\n\n if (stylesheets === null) {\n return null;\n }\n\n const mapping: {[key: string]: string} = {};\n for (const [name, url] of Object.entries(defaultStylesheets)) {\n mapping[name] = url;\n }\n for (const [name, url] of Object.entries(stylesheets)) {\n mapping[name] = url;\n }\n\n const active = localStorage.getItem(PARAM_KEY);\n if (!active) {\n localStorage.setItem(PARAM_KEY, \"default\");\n }\n\n const toggleStylesheet = (sheet: string) => {\n localStorage.setItem(PARAM_KEY, sheet);\n window.location.reload();\n };\n\n const items = [];\n for (const [name, url] of Object.entries(mapping)) {\n items.push({\n id: name,\n title: name,\n onClick: () => toggleStylesheet(name),\n active: ((!active && name === \"default\") || active === name)\n });\n }\n\n return (\n <WithTooltip\n placement=\"top\"\n trigger=\"click\"\n tooltip={<TooltipLinkList links={items} />}\n closeOnOutsideClick\n >\n <IconButton\n key={TOOL_ID}\n active={isActive}\n title=\"Activate Stylesheet\"\n >\n <PaintBrushIcon />\n </IconButton>\n </WithTooltip>\n );\n};\n\nexport default Tool;\n","import React from \"react\";\nimport { addons, types, useParameter } from \"storybook/manager-api\";\nimport { ADDON_ID, PARAM_KEY } from \"./constants\";\nimport Tool from './Tool'\n\naddons.register(ADDON_ID, () => {\n addons.add(ADDON_ID, {\n title: 'Stylesheet Toggle',\n type: types.TOOL,\n render: () => {\n const stylesheets = useParameter(\n PARAM_KEY,\n null,\n );\n\n return <Tool stylesheets={stylesheets} />\n },\n });\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/constants.ts","../src/defaults.ts","../src/customStylesheets.ts","../src/CustomStylesheetInput.tsx","../src/CustomTooltipLinkList.tsx","../src/Tool.tsx","../src/manager.tsx"],"names":["ADDON_ID","TOOL_ID","PARAM_KEY","defaultStylesheets","CUSTOM_KEY","getCustomStylesheets","data","parsed","sheet","saveCustomStylesheet","url","name","id","autoName","extractNameFromUrl","customSheets","finalName","ensureUniqueName","newSheet","mapping","acc","deleteCustomStylesheet","isValidStylesheetUrl","isDuplicateUrl","customStylesheets","parts","existing","existingNames","s","counter","CustomStylesheetInput","onAdd","inputValue","setInputValue","useState","error","setError","handleAdd","handleKeyDown","e","React","Form","Button","List","styled","ListItem","theme","active","DeleteButton","Separator","SectionLabel","ItemTitle","CustomTooltipLinkList","configuredStylesheets","activeStylesheet","onSelect","handleDelete","TrashIcon","Tool","stylesheets","globals","useGlobals","isActive","WithTooltip","IconButton","PaintBrushIcon","Tool_default","addons","types","useParameter"],"mappings":"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","file":"manager.js","sourcesContent":["export const ADDON_ID = \"stylesheetToggle\";\nexport const TOOL_ID = `${ADDON_ID}/tool`;\nexport const PARAM_KEY = `stylesheetToggle`;\n\n","export const defaultStylesheets: {[key: string]: string} = {\n \"default\": ''\n}\n","import { PARAM_KEY } from './constants';\n\nexport interface CustomStylesheet {\n id: string;\n name: string;\n url: string;\n}\n\nconst CUSTOM_KEY = `${PARAM_KEY}:custom`;\n\nexport function getCustomStylesheets(): CustomStylesheet[] {\n const data = localStorage.getItem(CUSTOM_KEY);\n if (!data) return [];\n\n try {\n const parsed = JSON.parse(data);\n\n // Validate structure\n if (!parsed || typeof parsed !== 'object') {\n throw new Error('Invalid format');\n }\n\n // Filter out invalid entries\n return Object.values(parsed).filter(\n (sheet: any): sheet is CustomStylesheet =>\n sheet &&\n typeof sheet.id === 'string' &&\n typeof sheet.name === 'string' &&\n typeof sheet.url === 'string'\n );\n } catch {\n // Clear corrupted data\n localStorage.removeItem(CUSTOM_KEY);\n return [];\n }\n}\n\nexport function saveCustomStylesheet(url: string, name?: string): CustomStylesheet {\n const id = `custom:${Date.now()}`;\n const autoName = name || extractNameFromUrl(url);\n const customSheets = getCustomStylesheets();\n\n // Ensure unique name\n const finalName = ensureUniqueName(autoName, customSheets);\n\n const newSheet: CustomStylesheet = { id, name: finalName, url };\n\n const allSheets = [...customSheets, newSheet];\n const mapping = allSheets.reduce((acc, sheet) => {\n acc[sheet.id] = sheet;\n return acc;\n }, {} as Record<string, CustomStylesheet>);\n\n localStorage.setItem(CUSTOM_KEY, JSON.stringify(mapping));\n return newSheet;\n}\n\nexport function deleteCustomStylesheet(id: string): void {\n const customSheets = getCustomStylesheets();\n const updated = customSheets.filter(sheet => sheet.id !== id);\n\n const mapping = updated.reduce((acc, sheet) => {\n acc[sheet.id] = sheet;\n return acc;\n }, {} as Record<string, CustomStylesheet>);\n\n localStorage.setItem(CUSTOM_KEY, JSON.stringify(mapping));\n\n // If deleted stylesheet was active, switch to default\n const active = localStorage.getItem(PARAM_KEY);\n if (active === id) {\n localStorage.setItem(PARAM_KEY, 'default');\n }\n}\n\nexport function isValidStylesheetUrl(url: string): boolean {\n // Must end with .css\n if (!url.endsWith('.css')) return false;\n\n try {\n if (url.startsWith('http://') || url.startsWith('https://')) {\n new URL(url); // Throws if invalid\n return true;\n }\n // Allow relative paths like ./theme.css or /styles/theme.css\n return url.startsWith('./') || url.startsWith('/') || !url.includes('://');\n } catch {\n return false;\n }\n}\n\nexport function isDuplicateUrl(url: string, customStylesheets: CustomStylesheet[]): boolean {\n return customStylesheets.some(sheet => sheet.url === url);\n}\n\nfunction extractNameFromUrl(url: string): string {\n // Extract filename from URL\n const parts = url.split('/');\n const filename = parts[parts.length - 1];\n return filename || 'Custom Stylesheet';\n}\n\nfunction ensureUniqueName(name: string, existing: CustomStylesheet[]): string {\n const existingNames = existing.map(s => s.name);\n if (!existingNames.includes(name)) return name;\n\n let counter = 2;\n while (existingNames.includes(`${name} (${counter})`)) {\n counter++;\n }\n return `${name} (${counter})`;\n}\n","import React, { useState } from 'react';\nimport { Form, Button } from 'storybook/internal/components';\nimport { saveCustomStylesheet, isValidStylesheetUrl, getCustomStylesheets, isDuplicateUrl } from './customStylesheets';\n\ninterface CustomStylesheetInputProps {\n onAdd: (id: string) => void;\n}\n\nexport const CustomStylesheetInput: React.FC<CustomStylesheetInputProps> = ({ onAdd }) => {\n const [inputValue, setInputValue] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const handleAdd = () => {\n // Validate URL\n if (!inputValue.trim()) {\n setError('URL is required');\n return;\n }\n\n if (!isValidStylesheetUrl(inputValue)) {\n setError('Invalid stylesheet URL (must end with .css)');\n return;\n }\n\n // Check duplicates\n const existing = getCustomStylesheets();\n if (isDuplicateUrl(inputValue, existing)) {\n setError('This stylesheet already exists');\n return;\n }\n\n // Save and switch to new stylesheet\n const newSheet = saveCustomStylesheet(inputValue);\n setInputValue('');\n setError(null);\n onAdd(newSheet.id);\n };\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n handleAdd();\n }\n };\n\n return (\n <div style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>\n <Form.Field label=\"Add Custom Stylesheet\">\n <div style={{ display: 'flex', gap: '4px' }}>\n <Form.Input\n value={inputValue}\n onChange={(e) => setInputValue(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder=\"https://example.com/theme.css\"\n style={{ flex: 1 }}\n />\n <Button onClick={handleAdd} size=\"small\">\n Add\n </Button>\n </div>\n {error && (\n <div style={{ color: 'red', fontSize: '12px', marginTop: '4px' }}>\n {error}\n </div>\n )}\n </Form.Field>\n </div>\n );\n};\n","import React from 'react';\nimport { styled } from 'storybook/theming';\nimport { TrashIcon } from '@storybook/icons';\nimport { CustomStylesheetInput } from './CustomStylesheetInput';\nimport { getCustomStylesheets, deleteCustomStylesheet } from './customStylesheets';\n\nconst List = styled.div({\n minWidth: 280,\n maxWidth: 400,\n overflow: 'hidden',\n overflowY: 'auto',\n maxHeight: '400px',\n});\n\nconst ListItem = styled.div<{ active?: boolean }>(({ theme, active }) => ({\n fontSize: theme.typography.size.s1,\n color: active ? theme.color.secondary : theme.color.dark,\n textDecoration: 'none',\n cursor: 'pointer',\n lineHeight: '18px',\n padding: '7px 10px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n backgroundColor: active ? theme.background.hoverable : 'transparent',\n '&:hover': {\n background: theme.background.hoverable,\n },\n}));\n\nconst DeleteButton = styled.button(({ theme }) => ({\n background: 'none',\n border: 'none',\n cursor: 'pointer',\n padding: '2px 4px',\n display: 'flex',\n alignItems: 'center',\n opacity: 0.6,\n '&:hover': {\n opacity: 1,\n color: theme.color.negative,\n },\n}));\n\nconst Separator = styled.div(({ theme }) => ({\n height: '1px',\n background: theme.appBorderColor,\n margin: '8px 0',\n}));\n\nconst SectionLabel = styled.div(({ theme }) => ({\n fontSize: '11px',\n color: theme.color.mediumdark,\n padding: '4px 10px',\n fontWeight: 'bold',\n textTransform: 'uppercase',\n}));\n\nconst ItemTitle = styled.span({\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n maxWidth: '200px',\n});\n\ninterface CustomTooltipLinkListProps {\n configuredStylesheets: { [key: string]: string };\n activeStylesheet: string;\n onSelect: (id: string) => void;\n}\n\nexport const CustomTooltipLinkList: React.FC<CustomTooltipLinkListProps> = ({\n configuredStylesheets,\n activeStylesheet,\n onSelect,\n}) => {\n const customStylesheets = getCustomStylesheets();\n\n const handleDelete = (e: React.MouseEvent, id: string) => {\n e.stopPropagation();\n if (confirm('Delete this custom stylesheet?')) {\n deleteCustomStylesheet(id);\n window.location.reload();\n }\n };\n\n return (\n <List>\n <CustomStylesheetInput onAdd={onSelect} />\n\n {customStylesheets.length > 0 && (\n <>\n <SectionLabel>Custom Stylesheets</SectionLabel>\n {customStylesheets.map((sheet) => (\n <ListItem\n key={sheet.id}\n active={activeStylesheet === sheet.id}\n onClick={() => onSelect(sheet.id)}\n >\n <ItemTitle title={sheet.url}>{sheet.name}</ItemTitle>\n <DeleteButton onClick={(e) => handleDelete(e, sheet.id)} aria-label=\"Delete stylesheet\">\n <TrashIcon size={12} />\n </DeleteButton>\n </ListItem>\n ))}\n <Separator />\n </>\n )}\n\n <SectionLabel>Configured Stylesheets</SectionLabel>\n {Object.entries(configuredStylesheets).map(([name]) => (\n <ListItem\n key={name}\n active={activeStylesheet === name}\n onClick={() => onSelect(name)}\n >\n {name}\n </ListItem>\n ))}\n </List>\n );\n};\n","import React from \"react\";\nimport { useGlobals } from \"storybook/manager-api\";\nimport { IconButton, WithTooltip } from \"storybook/internal/components\";\nimport { PaintBrushIcon } from \"@storybook/icons\";\nimport { PARAM_KEY, TOOL_ID } from \"./constants\";\nimport { defaultStylesheets } from \"./defaults\";\nimport { CustomTooltipLinkList } from \"./CustomTooltipLinkList\";\n\n\nconst Tool = ({ stylesheets }: { [key: string]: string }) => {\n const [globals] = useGlobals();\n const isActive = [true, \"true\"].includes(globals[PARAM_KEY]);\n\n if (stylesheets === null) {\n return null;\n }\n\n // Merge default and configured stylesheets\n const configuredStylesheets: {[key: string]: string} = {\n ...defaultStylesheets,\n ...stylesheets,\n };\n\n const activeStylesheet = localStorage.getItem(PARAM_KEY) || \"default\";\n\n const handleSelect = (id: string) => {\n localStorage.setItem(PARAM_KEY, id);\n window.location.reload();\n };\n\n return (\n <WithTooltip\n placement=\"top\"\n trigger=\"click\"\n tooltip={\n <CustomTooltipLinkList\n configuredStylesheets={configuredStylesheets}\n activeStylesheet={activeStylesheet}\n onSelect={handleSelect}\n />\n }\n closeOnOutsideClick\n >\n <IconButton\n key={TOOL_ID}\n active={isActive}\n title=\"Toggle Stylesheet\"\n >\n <PaintBrushIcon />\n </IconButton>\n </WithTooltip>\n );\n};\n\nexport default Tool;\n","import React from \"react\";\nimport { addons, types, useParameter } from \"storybook/manager-api\";\nimport { ADDON_ID, PARAM_KEY } from \"./constants\";\nimport Tool from './Tool'\n\naddons.register(ADDON_ID, () => {\n addons.add(ADDON_ID, {\n title: 'Stylesheet Toggle',\n type: types.TOOL,\n render: () => {\n const stylesheets = useParameter(\n PARAM_KEY,\n null,\n );\n\n return <Tool stylesheets={stylesheets} />\n },\n });\n});\n"]}
|
package/dist/preview.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {useGlobals,useEffect}from'storybook/preview-api';var
|
|
1
|
+
import {useGlobals,useEffect}from'storybook/preview-api';var o="stylesheetToggle";var c=`${o}:custom`;function a(){let s=localStorage.getItem(c);if(!s)return [];try{let t=JSON.parse(s);if(!t||typeof t!="object")throw new Error("Invalid format");return Object.values(t).filter(e=>e&&typeof e.id=="string"&&typeof e.name=="string"&&typeof e.url=="string")}catch{return localStorage.removeItem(c),[]}}var u=(s,t)=>{let[e]=useGlobals(),i=e[o],{theme:r}=t.globals,m=t.parameters[o],l=localStorage.getItem(o)||"default",n;return l.startsWith("custom:")?n=a().find(f=>f.id===l)?.url:n=m?.[l],useEffect(()=>{n&&S(n);},[i,r,n]),s()};function S(s){let t=document.querySelector("#stylesheetToggle"),e=document.querySelector("#storybook-root"),i=document.querySelector("body"),r=document.createElement("link");s&&(r.setAttribute("id","stylesheetToggle"),r.setAttribute("rel","stylesheet"),r.setAttribute("href",s),t?.remove(),i.insertBefore(r,e));}var d={decorators:[u],initialGlobals:{[o]:false}},P=d;export{P as default};//# sourceMappingURL=preview.js.map
|
|
2
2
|
//# sourceMappingURL=preview.js.map
|
package/dist/preview.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/constants.ts","../src/withGlobals.ts","../src/preview.ts"],"names":["PARAM_KEY","withGlobals","StoryFn","context","globals","useGlobals","stylesheetToggle","theme","
|
|
1
|
+
{"version":3,"sources":["../src/constants.ts","../src/customStylesheets.ts","../src/withGlobals.ts","../src/preview.ts"],"names":["PARAM_KEY","CUSTOM_KEY","getCustomStylesheets","data","parsed","sheet","withGlobals","StoryFn","context","globals","useGlobals","stylesheetToggle","theme","configuredStylesheets","activeId","stylesheetUrl","s","useEffect","injectStylesheet","stylesheet","previousStylesheet","beforeElement","bodyElement","stylesheetElement","preview","preview_default"],"mappings":"yDAAO,IAEMA,CAAAA,CAAY,mBCMzB,IAAMC,CAAAA,CAAa,CAAA,EAAGD,CAAS,UAExB,SAASE,CAAAA,EAA2C,CACzD,IAAMC,CAAAA,CAAO,YAAA,CAAa,QAAQF,CAAU,CAAA,CAC5C,GAAI,CAACE,CAAAA,CAAM,OAAO,EAAC,CAEnB,GAAI,CACF,IAAMC,CAAAA,CAAS,IAAA,CAAK,MAAMD,CAAI,CAAA,CAG9B,GAAI,CAACC,CAAAA,EAAU,OAAOA,GAAW,QAAA,CAC/B,MAAM,IAAI,KAAA,CAAM,gBAAgB,CAAA,CAIlC,OAAO,MAAA,CAAO,MAAA,CAAOA,CAAM,CAAA,CAAE,MAAA,CAC1BC,CAAAA,EACCA,GACA,OAAOA,CAAAA,CAAM,EAAA,EAAO,QAAA,EACpB,OAAOA,CAAAA,CAAM,MAAS,QAAA,EACtB,OAAOA,CAAAA,CAAM,GAAA,EAAQ,QACzB,CACF,MAAQ,CAEN,OAAA,YAAA,CAAa,UAAA,CAAWJ,CAAU,CAAA,CAC3B,EACT,CACF,CC7BO,IAAMK,CAAAA,CAAc,CACzBC,CAAAA,CACAC,IACG,CACH,GAAM,CAACC,CAAO,CAAA,CAAIC,UAAAA,GACZC,CAAAA,CAAmBF,CAAAA,CAAQT,CAAS,CAAA,CAEpC,CAAE,KAAA,CAAAY,CAAM,CAAA,CAAIJ,CAAAA,CAAQ,OAAA,CACpBK,CAAAA,CAAwBL,CAAAA,CAAQ,UAAA,CAAWR,CAAS,CAAA,CAEpDc,CAAAA,CAAW,YAAA,CAAa,OAAA,CAAQd,CAAS,CAAA,EAAK,UAGhDe,CAAAA,CAEJ,OAAID,CAAAA,CAAS,UAAA,CAAW,SAAS,CAAA,CAI/BC,EAFqBb,CAAAA,EAAqB,CACT,IAAA,CAAKc,CAAAA,EAAKA,CAAAA,CAAE,EAAA,GAAOF,CAAQ,CAAA,EAC/B,GAAA,CAG7BC,CAAAA,CAAgBF,CAAAA,GAAwBC,CAAQ,CAAA,CAGlDG,UAAU,IAAM,CACVF,CAAAA,EACFG,CAAAA,CAAiBH,CAAa,EAElC,EAAG,CAACJ,CAAAA,CAAkBC,CAAAA,CAAOG,CAAa,CAAC,CAAA,CAEpCR,GACT,CAAA,CAEA,SAASW,CAAAA,CAAiBC,CAAAA,CAAoB,CAC5C,IAAMC,CAAAA,CAAqB,QAAA,CAAS,aAAA,CAAc,mBAAmB,CAAA,CAC/DC,CAAAA,CAAgB,SAAS,aAAA,CAAc,iBAAiB,CAAA,CACxDC,CAAAA,CAAc,QAAA,CAAS,aAAA,CAAc,MAAM,CAAA,CAC3CC,CAAAA,CAAoB,QAAA,CAAS,aAAA,CAAc,MAAM,CAAA,CAElDJ,IAILI,CAAAA,CAAkB,YAAA,CAAa,IAAA,CAAM,kBAAkB,CAAA,CACvDA,CAAAA,CAAkB,aAAa,KAAA,CAAO,YAAY,CAAA,CAClDA,CAAAA,CAAkB,YAAA,CAAa,MAAA,CAAQJ,CAAU,CAAA,CAEjDC,CAAAA,EAAoB,MAAA,EAAO,CAC3BE,CAAAA,CAAY,YAAA,CAAaC,EAAmBF,CAAa,CAAA,EAC3D,CCnDA,IAAMG,CAAAA,CAAwC,CAC5C,WAAY,CAAClB,CAAW,CAAA,CACxB,cAAA,CAAgB,CACd,CAACN,CAAS,EAAG,KACf,CACF,CAAA,CAEOyB,CAAAA,CAAQD","file":"preview.js","sourcesContent":["export const ADDON_ID = \"stylesheetToggle\";\nexport const TOOL_ID = `${ADDON_ID}/tool`;\nexport const PARAM_KEY = `stylesheetToggle`;\n\n","import { PARAM_KEY } from './constants';\n\nexport interface CustomStylesheet {\n id: string;\n name: string;\n url: string;\n}\n\nconst CUSTOM_KEY = `${PARAM_KEY}:custom`;\n\nexport function getCustomStylesheets(): CustomStylesheet[] {\n const data = localStorage.getItem(CUSTOM_KEY);\n if (!data) return [];\n\n try {\n const parsed = JSON.parse(data);\n\n // Validate structure\n if (!parsed || typeof parsed !== 'object') {\n throw new Error('Invalid format');\n }\n\n // Filter out invalid entries\n return Object.values(parsed).filter(\n (sheet: any): sheet is CustomStylesheet =>\n sheet &&\n typeof sheet.id === 'string' &&\n typeof sheet.name === 'string' &&\n typeof sheet.url === 'string'\n );\n } catch {\n // Clear corrupted data\n localStorage.removeItem(CUSTOM_KEY);\n return [];\n }\n}\n\nexport function saveCustomStylesheet(url: string, name?: string): CustomStylesheet {\n const id = `custom:${Date.now()}`;\n const autoName = name || extractNameFromUrl(url);\n const customSheets = getCustomStylesheets();\n\n // Ensure unique name\n const finalName = ensureUniqueName(autoName, customSheets);\n\n const newSheet: CustomStylesheet = { id, name: finalName, url };\n\n const allSheets = [...customSheets, newSheet];\n const mapping = allSheets.reduce((acc, sheet) => {\n acc[sheet.id] = sheet;\n return acc;\n }, {} as Record<string, CustomStylesheet>);\n\n localStorage.setItem(CUSTOM_KEY, JSON.stringify(mapping));\n return newSheet;\n}\n\nexport function deleteCustomStylesheet(id: string): void {\n const customSheets = getCustomStylesheets();\n const updated = customSheets.filter(sheet => sheet.id !== id);\n\n const mapping = updated.reduce((acc, sheet) => {\n acc[sheet.id] = sheet;\n return acc;\n }, {} as Record<string, CustomStylesheet>);\n\n localStorage.setItem(CUSTOM_KEY, JSON.stringify(mapping));\n\n // If deleted stylesheet was active, switch to default\n const active = localStorage.getItem(PARAM_KEY);\n if (active === id) {\n localStorage.setItem(PARAM_KEY, 'default');\n }\n}\n\nexport function isValidStylesheetUrl(url: string): boolean {\n // Must end with .css\n if (!url.endsWith('.css')) return false;\n\n try {\n if (url.startsWith('http://') || url.startsWith('https://')) {\n new URL(url); // Throws if invalid\n return true;\n }\n // Allow relative paths like ./theme.css or /styles/theme.css\n return url.startsWith('./') || url.startsWith('/') || !url.includes('://');\n } catch {\n return false;\n }\n}\n\nexport function isDuplicateUrl(url: string, customStylesheets: CustomStylesheet[]): boolean {\n return customStylesheets.some(sheet => sheet.url === url);\n}\n\nfunction extractNameFromUrl(url: string): string {\n // Extract filename from URL\n const parts = url.split('/');\n const filename = parts[parts.length - 1];\n return filename || 'Custom Stylesheet';\n}\n\nfunction ensureUniqueName(name: string, existing: CustomStylesheet[]): string {\n const existingNames = existing.map(s => s.name);\n if (!existingNames.includes(name)) return name;\n\n let counter = 2;\n while (existingNames.includes(`${name} (${counter})`)) {\n counter++;\n }\n return `${name} (${counter})`;\n}\n","import type { PartialStoryFn as StoryFunction, Renderer, StoryContext } from \"storybook/internal/types\";\nimport { useEffect, useGlobals } from \"storybook/preview-api\";\nimport { PARAM_KEY } from \"./constants\";\nimport { getCustomStylesheets } from \"./customStylesheets\";\n\n\nexport const withGlobals = (\n StoryFn: StoryFunction<Renderer>,\n context: StoryContext<Renderer>\n) => {\n const [globals] = useGlobals();\n const stylesheetToggle = globals[PARAM_KEY];\n\n const { theme } = context.globals;\n const configuredStylesheets = context.parameters[PARAM_KEY];\n\n const activeId = localStorage.getItem(PARAM_KEY) || \"default\";\n\n // Resolve URL: check if it's a custom stylesheet or configured stylesheet\n let stylesheetUrl: string | undefined;\n\n if (activeId.startsWith('custom:')) {\n // Look up in custom stylesheets\n const customSheets = getCustomStylesheets();\n const customSheet = customSheets.find(s => s.id === activeId);\n stylesheetUrl = customSheet?.url;\n } else {\n // Look up in configured stylesheets\n stylesheetUrl = configuredStylesheets?.[activeId];\n }\n\n useEffect(() => {\n if (stylesheetUrl) {\n injectStylesheet(stylesheetUrl);\n }\n }, [stylesheetToggle, theme, stylesheetUrl]);\n\n return StoryFn();\n};\n\nfunction injectStylesheet(stylesheet: string) {\n const previousStylesheet = document.querySelector(\"#stylesheetToggle\");\n const beforeElement = document.querySelector(\"#storybook-root\");\n const bodyElement = document.querySelector(\"body\");\n const stylesheetElement = document.createElement(\"link\");\n\n if (!stylesheet) {\n return;\n }\n\n stylesheetElement.setAttribute(\"id\", \"stylesheetToggle\");\n stylesheetElement.setAttribute(\"rel\", \"stylesheet\");\n stylesheetElement.setAttribute(\"href\", stylesheet);\n\n previousStylesheet?.remove();\n bodyElement.insertBefore(stylesheetElement, beforeElement);\n}\n","import type { Renderer, ProjectAnnotations } from \"storybook/internal/types\";\nimport { PARAM_KEY } from \"./constants\";\nimport { withGlobals } from \"./withGlobals\";\n\n\nconst preview: ProjectAnnotations<Renderer> = {\n decorators: [withGlobals],\n initialGlobals: {\n [PARAM_KEY]: false,\n },\n};\n\nexport default preview;\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stevendejong/storybook-stylesheet-toggle",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Toggle between stylesheets in Storybook. Configure stylesheets via parameters or add custom ones dynamically at runtime.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"storybook-addons"
|
|
7
7
|
],
|