Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions storybook/source/.storybook/manager.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
import { addons } from 'storybook/manager-api';
import { themes } from 'storybook/theming';
import { create } from 'storybook/theming/create';
import '../styles/manager.css';

const onlyofficeTheme = create({
base: 'dark',
colorPrimary: '#4D9DFF',
colorSecondary: '#4D9DFF',
appBg: '#1E1E1E',
appContentBg: '#252525',
appBorderColor: 'rgba(255, 255, 255, 0.12)',
appBorderRadius: 10,
barBg: '#1E1E1E',
barTextColor: '#A0A0A0',
barSelectedColor: '#4D9DFF',
barHoverColor: '#E0E0E0',
textColor: '#DEDEDE',
textInverseColor: '#1E1E1E',
inputBg: '#333333',
inputBorder: '#555555',
inputTextColor: '#DEDEDE',
brandTitle: 'ONLYOFFICE Plugin UI',
brandUrl: 'https://www.onlyoffice.com/',
brandTarget: '_self',
});

addons.setConfig({
theme: {
...themes.normal,
brandImage: './logo.svg',
brandUrl: 'https://onlyoffice.github.io/',
theme: onlyofficeTheme,
sidebar: {
showRoots: false,
},
});
215 changes: 179 additions & 36 deletions storybook/source/.storybook/preview.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,198 @@
import '../styles/preview.css';
import '../styles/dialog-button.css';
import '../styles/plugin-buttons.css';
import '../styles/cards.css';
import '../styles/checkbox.css';
import '../styles/radio.css';
import '../styles/common-controls.css';
import '../styles/textarea.css';

const themeTokens = {
'Light': {
pageBg: '#FFFFFF',
pageSurface: '#F7F7F7',
pageSurfaceAlt: '#F9F9F9',
pageBorder: '#DFDFDF',
pageFg: 'rgba(0, 0, 0, 0.80)',
pageMuted: 'rgba(0, 0, 0, 0.60)',
},
'Light Classic': {
pageBg: '#FFFFFF',
pageSurface: '#F1F1F1',
pageSurfaceAlt: '#D8DADC',
pageBorder: '#CBCBCB',
pageFg: '#444444',
pageMuted: '#A5A5A5',
},
'Modern Light': {
pageBg: '#FFFFFF',
pageSurface: '#F3F3F3',
pageSurfaceAlt: '#F9F9F9',
pageBorder: '#E1E1E1',
pageFg: '#383838',
pageMuted: undefined,
},
'Modern Dark': {
pageBg: '#404040',
pageSurface: '#404040',
pageSurfaceAlt: '#585858',
pageBorder: '#686868',
pageFg: '#E8E8E8',
pageMuted: 'rgba(232, 232, 232, 0.70)',
},
'Dark': {
pageBg: '#333333',
pageSurface: '#333333',
pageSurfaceAlt: '#555555',
pageBorder: '#666666',
pageFg: 'rgba(255, 255, 255, 0.80)',
pageMuted: 'rgba(255, 255, 255, 0.60)',
},
'Dark Contrast': {
pageBg: '#1E1E1E',
pageSurface: '#1E1E1E',
pageSurfaceAlt: '#424242',
pageBorder: '#696969',
pageFg: '#E8E8E8',
pageMuted: '#B8B8B8',
},
};

const THEME_ALIASES = {
light: 'Light',
lightclassic: 'Light Classic',
modernlight: 'Modern Light',
moderndark: 'Modern Dark',
dark: 'Dark',
darkcontrast: 'Dark Contrast',
};

function normalizeThemeName(rawTheme) {
if (!rawTheme) return 'Light';
const cleaned = decodeURIComponent(String(rawTheme)).replace(/^!/, '').trim();
const compact = cleaned.replace(/[\s_-]+/g, '').toLowerCase();
return THEME_ALIASES[compact] ?? 'Light';
}

/** @type { import('@storybook/html-vite').Preview } */
const preview = {
/* globalTypes: {
globalTypes: {
theme: {
description: 'Theme switcher',
defaultValue: 'light',
name: 'Theme',
description: 'Global plugin UI theme',
defaultValue: 'Light',
toolbar: {
title: 'Theme',
icon: 'paintbrush',
items: [
{ value: 'light', icon: 'sun', title: 'light' },
{ value: 'dark', icon: 'moon', title: 'dark' }
'Light',
'Light Classic',
'Dark',
'Dark Contrast',
'Modern Light',
'Modern Dark',
],
dynamicTitle: true,
},
},
},*/
},

parameters: {
backgrounds: { disable: true },
options: {
storySort: {
order: [
'Foundations',
['Typography', 'Colors', 'Icons'],
'Components',
[
'Buttons',
['Dialog Buttons', 'Panel Buttons', 'Icon Buttons', 'Link Buttons', 'Split Buttons'],
'Form',
['Checkbox', 'Radio', 'Switches', 'Slider', 'Text Field', 'Text Area'],
'Data Display',
['Cards', 'Info Block', 'Tabs'],
'Layout',
['Header', 'Modal Window', 'Scroll'],
'Feedback',
['Loader', 'Tooltip'],
'Actions',
['Context Menu', 'Preview Controls'],
],
'*',
],
},
},
docs: {
toc: true,
story: { inline: true },
canvas: { withToolbar: true },
},
controls: {
expanded: true,
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};

export const decorators = [
(storyFn, context) => {
const theme = context.globals.theme || 'light';

// Apply the theme as a data attribute
document.documentElement.setAttribute('data-theme', theme);

// Set window.Asc.plugin.theme for OnlyOffice plugin compatibility
if (!window.Asc) {
window.Asc = {};
}
if (!window.Asc.plugin) {
window.Asc.plugin = {};
}
window.Asc.plugin.theme = {
type: theme
};

return storyFn();
},
];

const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.css';
document.head.appendChild(link);
decorators: [
(storyFn, context) => {
const theme = normalizeThemeName(context.globals.theme);
const tokens = themeTokens[theme] || themeTokens['Light'];
const isDark = theme === 'Dark' || theme === 'Dark Contrast' || theme === 'Modern Dark';
const isDocs = context.viewMode === 'docs';

const pageBg = tokens.pageBg;
const pageFg = tokens.pageFg;
const pageSurface = tokens.pageSurface;
const pageSurfaceAlt = tokens.pageSurfaceAlt;
const pageBorder = tokens.pageBorder;
const pageMuted = tokens.pageMuted ?? (isDark ? 'rgba(255, 255, 255, 0.60)' : 'rgba(0, 0, 0, 0.60)');
const pageAccent = isDark ? '#4D9DFF' : '#0B6DFF';

const root = document.documentElement;
root.style.setProperty('--page-bg', pageBg);
root.style.setProperty('--page-fg', pageFg);
root.style.setProperty('--page-border', pageBorder);
root.style.setProperty('--page-surface', pageSurface);
root.style.setProperty('--page-surface-alt', pageSurfaceAlt);
root.style.setProperty('--page-muted', pageMuted);
root.style.setProperty('--page-accent', pageAccent);
root.dataset.pluginTheme = theme.replace(/\s+/g, '-').toLowerCase();

// OnlyOffice plugin theme compatibility
if (!window.Asc) window.Asc = {};
if (!window.Asc.plugin) window.Asc.plugin = {};
window.Asc.plugin.theme = { type: theme };

const wrapper = document.createElement('div');
wrapper.style.cssText = [
`background-color:${pageBg}`,
`color:${pageFg}`,
`padding:${isDocs ? '0' : '20px'}`,
'min-height:0',
'width:100%',
`--page-bg:${pageBg}`,
`--page-fg:${pageFg}`,
`--page-border:${pageBorder}`,
`--page-surface:${pageSurface}`,
`--page-surface-alt:${pageSurfaceAlt}`,
`--page-muted:${pageMuted}`,
`--page-accent:${pageAccent}`,
].join(';');

const storyEl = storyFn();
if (typeof storyEl === 'string') {
wrapper.innerHTML = storyEl;
} else if (storyEl instanceof Node) {
wrapper.appendChild(storyEl);
} else {
wrapper.textContent = String(storyEl ?? '');
}
return wrapper;
},
],
};

export default preview;
72 changes: 72 additions & 0 deletions storybook/source/components/Card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { resolveTheme } from './theme-utils.js';
import { cardsByTheme } from '../data/cards.ts';

const CHEVRON_DOWN = (color) => `<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M6 8L10 12L14 8" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
const CHEVRON_UP = (color) => `<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M14 12L10 8L6 12" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;

const DEFAULT_TITLE = "His English teacher says that he is a good pupil and a great student who always participates in class discussions and finishes his work carefully";
const DEFAULT_TAGS = ["His", "Her", "Label", "Label", "Label", "Label"];

export function Card({
type = 'close',
state = 'default',
theme,
minWidth = 236,
title = DEFAULT_TITLE,
helperText = 'Text here',
actionLabel = 'Button',
tags = DEFAULT_TAGS,
} = {}) {
const resolved = resolveTheme(theme);
const tokens = cardsByTheme[resolved] ?? cardsByTheme['Light'];
const isModern = resolved.startsWith('Modern');
const isHover = state === 'hover';
const isExpanded = type !== 'close';

const fontSize = isModern ? 12 : 11;

const containerBg = (type === 'close' && isHover) ? tokens.closeHoverBackground : tokens.background;

const containerStyle = [
`width:${minWidth}px`, `min-width:${minWidth}px`, `padding:6px`,
`border-radius:${tokens.radius}px`, `border:1px solid ${tokens.border}`,
`background:${containerBg}`, `display:inline-flex`,
`flex-direction:${type === 'close' ? 'row' : 'column'}`,
`align-items:${type === 'close' ? 'center' : 'flex-start'}`,
`justify-content:center`, `gap:${type === 'close' ? 4 : 10}px`,
`box-sizing:border-box`,
].join(';');

const textStyle = `flex:1 1 0;min-width:0;font-size:${fontSize}px;font-family:Arial,Helvetica,sans-serif;font-weight:400;line-height:12px;letter-spacing:${isModern ? 0.24 : 0.22}px;color:${tokens.textColor};${isExpanded ? 'display:block;white-space:normal;overflow-wrap:anywhere;' : 'overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;'}`;

const chevron = isExpanded ? CHEVRON_UP(tokens.iconColor) : CHEVRON_DOWN(tokens.iconColor);

const titleRow = `<div style="width:100%;display:flex;align-items:${isExpanded ? 'flex-start' : 'center'};gap:4px;">
<div style="${textStyle}">${title}</div>
${chevron}
</div>`;

let expandedContent = '';
if (type === 'openWithButton') {
const chips = tags.map(tag => `<div data-chip="true" data-rest-bg="${tokens.chipBackground}" data-hover-bg="${tokens.closeHoverBackground}" style="height:24px;min-width:48px;padding:0 12px;border-radius:31px;border:1px solid ${tokens.chipBorder};background:${tokens.chipBackground};color:${tokens.chipTextColor};font-size:${fontSize}px;font-family:Arial,Helvetica,sans-serif;font-weight:400;line-height:16px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;">${tag}</div>`).join('');
const actionBg = isHover ? tokens.actionPrimaryHoverBackground : tokens.actionPrimaryBackground;
expandedContent = `
<div style="width:100%;display:flex;flex-wrap:wrap;gap:8px;">${chips}</div>
<div style="width:100%;">
<button type="button" data-btn-type="primary" data-rest-bg="${tokens.actionPrimaryBackground}" data-hover-bg="${tokens.actionPrimaryHoverBackground}" style="width:100%;height:${tokens.actionHeight}px;padding:0 32px;border:none;border-radius:${tokens.radius === 4 ? 4 : 1}px;background:${actionBg};color:${tokens.actionPrimaryTextColor};display:inline-flex;align-items:center;justify-content:center;cursor:pointer;">
<span style="text-align:center;font-size:${fontSize}px;font-family:Arial,Helvetica,sans-serif;font-weight:700;line-height:16px;letter-spacing:${isModern ? 0.24 : 0.22}px;white-space:nowrap;color:${tokens.actionPrimaryTextColor};">${actionLabel}</span>
</button>
</div>`;
} else if (type === 'openWithText') {
const actionBg = isHover ? tokens.actionSecondaryHoverBackground : tokens.actionSecondaryBackground;
expandedContent = `
<div style="width:100%;color:${tokens.subTextColor};font-size:${fontSize}px;font-family:Arial,Helvetica,sans-serif;font-weight:400;line-height:12px;letter-spacing:${isModern ? 0.24 : 0.22}px;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;">${helperText}</div>
<div style="width:100%;">
<button type="button" data-btn-type="secondary" data-rest-bg="${tokens.actionSecondaryBackground}" data-hover-bg="${tokens.actionSecondaryHoverBackground}" style="width:100%;height:${tokens.actionHeight}px;padding:0 32px;border-radius:${tokens.radius === 4 ? 4 : 1}px;border:1px solid ${tokens.actionSecondaryBorder};background:${actionBg};color:${tokens.actionSecondaryTextColor};display:inline-flex;align-items:center;justify-content:center;cursor:pointer;">
<span style="text-align:center;font-size:${fontSize}px;font-family:Arial,Helvetica,sans-serif;font-weight:700;line-height:16px;letter-spacing:${isModern ? 0.24 : 0.22}px;white-space:nowrap;color:${tokens.actionSecondaryTextColor};">${actionLabel}</span>
</button>
</div>`;
}

return `<div class="ui-card" style="${containerStyle}">${titleRow}${expandedContent}</div>`;
}
40 changes: 40 additions & 0 deletions storybook/source/components/Checkbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { checkboxTokens, resolveCheckboxTheme } from '../data/checkbox.ts';

const CHECK_PATH = 'M2.75 6.94995L5.75 9.74995L11.25 4.25005';

/**
* Checkbox — HTML-rendering version
* @param {object} props
* @param {string} [props.label]
* @param {'no'|'yes'|'partial'} [props.selected]
* @param {'default'|'hover'|'disabled'} [props.state]
* @param {string} [props.theme]
* @param {boolean} [props.truncate] - Truncate long label with ellipsis instead of wrapping
*/
export function Checkbox({ label = 'Text', selected = 'no', state = 'default', theme = 'Light', truncate = false } = {}) {
const resolvedTheme = resolveCheckboxTheme(theme);
const token = checkboxTokens[resolvedTheme][state][selected];
const isDisabled = state === 'disabled';
const fontSize = resolvedTheme.startsWith('Modern') ? 12 : 11;
const letterSpacing = resolvedTheme.startsWith('Modern') ? 0.24 : 0.22;

const checkMark = selected === 'yes' && token.markColor
? `<path d="${CHECK_PATH}" stroke="${token.markColor}" stroke-opacity="${token.markOpacity ?? 1}" stroke-width="2"/>`
: '';
const partialMark = selected === 'partial' && token.markColor
? `<rect x="3" y="6" width="8" height="2" fill="${token.markColor}" fill-opacity="${token.markOpacity ?? 1}"/>`
: '';

return `<button type="button" role="checkbox" aria-checked="${selected === 'partial' ? 'mixed' : selected === 'yes'}"
aria-disabled="${isDisabled}" class="ui-checkbox${isDisabled ? ' ui-checkbox--disabled' : ''}"
style="justify-content:flex-start;align-items:center;gap:8px;display:inline-flex;"
${isDisabled ? 'disabled' : ''}>
<span style="padding-top:2px;padding-bottom:2px;display:flex;align-items:center;">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
<rect x="0.5" y="0.5" width="13" height="13" rx="${token.boxRadius}" fill="${token.boxFill}" stroke="${token.boxStroke}"/>
${checkMark}${partialMark}
</svg>
</span>
<span style="color:${token.textColor};font-size:${fontSize}px;font-family:Arial,Helvetica,sans-serif;font-weight:400;line-height:16px;letter-spacing:${letterSpacing}px;text-align:left;${truncate ? 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' : 'word-wrap:break-word;'}">${label}</span>
</button>`;
}
Loading