Summary: Custom widgets are now shown in a gallery. The gallery is automatically opened when a new custom widget is added to a page. Descriptions, authors, and update times are pulled from the widget manifest. Test Plan: Browser tests. Reviewers: jarek Reviewed By: jarek Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D4309pull/1150/head
parent
a16d76d25d
commit
e70c294e3d
@ -0,0 +1,661 @@
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {textInput} from 'app/client/ui/inputs';
|
||||
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
||||
import {withInfoTooltip} from 'app/client/ui/tooltips';
|
||||
import {bigBasicButton, bigPrimaryButton} from 'app/client/ui2018/buttons';
|
||||
import {theme} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {cssLink} from 'app/client/ui2018/links';
|
||||
import {loadingSpinner} from 'app/client/ui2018/loaders';
|
||||
import {IModalControl, modal} from 'app/client/ui2018/modals';
|
||||
import {AccessLevel, ICustomWidget, matchWidget, WidgetAuthor} from 'app/common/CustomWidget';
|
||||
import {commonUrls} from 'app/common/gristUrls';
|
||||
import {bundleChanges, Computed, Disposable, dom, makeTestId, Observable, styled} from 'grainjs';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
|
||||
const testId = makeTestId('test-custom-widget-gallery-');
|
||||
|
||||
const t = makeT('CustomWidgetGallery');
|
||||
|
||||
export const CUSTOM_URL_WIDGET_ID = 'custom';
|
||||
|
||||
interface Options {
|
||||
sectionRef?: number;
|
||||
addWidget?(): Promise<{viewRef: number, sectionRef: number}>;
|
||||
}
|
||||
|
||||
export function showCustomWidgetGallery(gristDoc: GristDoc, options: Options = {}) {
|
||||
modal((ctl) => [
|
||||
dom.create(CustomWidgetGallery, ctl, gristDoc, options),
|
||||
cssModal.cls(''),
|
||||
]);
|
||||
}
|
||||
|
||||
interface WidgetInfo {
|
||||
variant: WidgetVariant;
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
developer?: WidgetAuthor;
|
||||
lastUpdated?: string;
|
||||
}
|
||||
|
||||
interface CustomWidgetACItem extends ICustomWidget {
|
||||
cleanText: string;
|
||||
}
|
||||
|
||||
type WidgetVariant = 'custom' | 'grist' | 'community';
|
||||
|
||||
class CustomWidgetGallery extends Disposable {
|
||||
private readonly _customUrl: Observable<string>;
|
||||
private readonly _filteredWidgets = Observable.create<ICustomWidget[] | null>(this, null);
|
||||
private readonly _section: ViewSectionRec | null = null;
|
||||
private readonly _searchText = Observable.create(this, '');
|
||||
private readonly _saveDisabled: Computed<boolean>;
|
||||
private readonly _savedWidgetId: Computed<string | null>;
|
||||
private readonly _selectedWidgetId = Observable.create<string | null>(this, null);
|
||||
private readonly _widgets = Observable.create<CustomWidgetACItem[] | null>(this, null);
|
||||
|
||||
constructor(
|
||||
private _ctl: IModalControl,
|
||||
private _gristDoc: GristDoc,
|
||||
private _options: Options = {}
|
||||
) {
|
||||
super();
|
||||
|
||||
const {sectionRef} = _options;
|
||||
if (sectionRef) {
|
||||
const section = this._gristDoc.docModel.viewSections.getRowModel(sectionRef);
|
||||
if (!section.id.peek()) {
|
||||
throw new Error(`Section ${sectionRef} does not exist`);
|
||||
}
|
||||
|
||||
this._section = section;
|
||||
this.autoDispose(section._isDeleted.subscribe((isDeleted) => {
|
||||
if (isDeleted) { this._ctl.close(); }
|
||||
}));
|
||||
}
|
||||
|
||||
let customUrl = '';
|
||||
if (this._section) {
|
||||
customUrl = this._section.customDef.url() ?? '';
|
||||
}
|
||||
this._customUrl = Observable.create(this, customUrl);
|
||||
|
||||
this._savedWidgetId = Computed.create(this, (use) => {
|
||||
if (!this._section) { return null; }
|
||||
|
||||
const {customDef} = this._section;
|
||||
// May be stored in one of two places, depending on age of document.
|
||||
const widgetId = use(customDef.widgetId) || use(customDef.widgetDef)?.widgetId;
|
||||
if (widgetId) {
|
||||
const pluginId = use(customDef.pluginId);
|
||||
const widget = matchWidget(use(this._widgets) ?? [], {
|
||||
widgetId,
|
||||
pluginId,
|
||||
});
|
||||
return widget ? `${pluginId}:${widgetId}` : null;
|
||||
} else {
|
||||
return CUSTOM_URL_WIDGET_ID;
|
||||
}
|
||||
});
|
||||
|
||||
this._saveDisabled = Computed.create(this, use => {
|
||||
const selectedWidgetId = use(this._selectedWidgetId);
|
||||
if (!selectedWidgetId) { return true; }
|
||||
if (!this._section) { return false; }
|
||||
|
||||
const savedWidgetId = use(this._savedWidgetId);
|
||||
if (selectedWidgetId === CUSTOM_URL_WIDGET_ID) {
|
||||
return (
|
||||
use(this._savedWidgetId) === CUSTOM_URL_WIDGET_ID &&
|
||||
use(this._customUrl) === use(this._section.customDef.url)
|
||||
);
|
||||
} else {
|
||||
return selectedWidgetId === savedWidgetId;
|
||||
}
|
||||
});
|
||||
|
||||
this._initializeWidgets().catch(reportError);
|
||||
|
||||
this.autoDispose(this._searchText.addListener(() => {
|
||||
this._filterWidgets();
|
||||
this._selectedWidgetId.set(null);
|
||||
}));
|
||||
}
|
||||
|
||||
public buildDom() {
|
||||
return cssCustomWidgetGallery(
|
||||
cssHeader(
|
||||
cssTitle(t('Choose Custom Widget')),
|
||||
cssSearchInputWrapper(
|
||||
cssSearchIcon('Search'),
|
||||
cssSearchInput(
|
||||
this._searchText,
|
||||
{placeholder: t('Search')},
|
||||
(el) => { setTimeout(() => el.focus(), 10); },
|
||||
testId('search'),
|
||||
),
|
||||
),
|
||||
),
|
||||
shadowScroll(
|
||||
this._buildWidgets(),
|
||||
cssShadowScroll.cls(''),
|
||||
),
|
||||
cssFooter(
|
||||
dom('div',
|
||||
cssHelpLink(
|
||||
{href: commonUrls.helpCustomWidgets, target: '_blank'},
|
||||
cssHelpIcon('Question'),
|
||||
t('Learn more about Custom Widgets'),
|
||||
),
|
||||
),
|
||||
cssFooterButtons(
|
||||
bigBasicButton(
|
||||
t('Cancel'),
|
||||
dom.on('click', () => this._ctl.close()),
|
||||
testId('cancel'),
|
||||
),
|
||||
bigPrimaryButton(
|
||||
this._options.addWidget ? t('Add Widget') : t('Change Widget'),
|
||||
dom.on('click', () => this._save()),
|
||||
dom.boolAttr('disabled', this._saveDisabled),
|
||||
testId('save'),
|
||||
),
|
||||
),
|
||||
),
|
||||
dom.onKeyDown({
|
||||
Enter: () => this._save(),
|
||||
Escape: () => this._deselectOrClose(),
|
||||
}),
|
||||
dom.on('click', (ev) => this._maybeClearSelection(ev)),
|
||||
testId('container'),
|
||||
);
|
||||
}
|
||||
|
||||
private async _initializeWidgets() {
|
||||
const widgets: ICustomWidget[] = [
|
||||
{
|
||||
widgetId: 'custom',
|
||||
name: t('Custom URL'),
|
||||
description: t('Add a widget from outside this gallery.'),
|
||||
url: '',
|
||||
},
|
||||
];
|
||||
try {
|
||||
const remoteWidgets = await this._gristDoc.appModel.topAppModel.getWidgets();
|
||||
if (this.isDisposed()) { return; }
|
||||
|
||||
widgets.push(...remoteWidgets
|
||||
.filter(({published}) => published !== false)
|
||||
.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
} catch (e) {
|
||||
reportError(e);
|
||||
}
|
||||
|
||||
this._widgets.set(widgets.map(w => ({...w, cleanText: getWidgetCleanText(w)})));
|
||||
this._selectedWidgetId.set(this._savedWidgetId.get());
|
||||
this._filterWidgets();
|
||||
}
|
||||
|
||||
private _filterWidgets() {
|
||||
const widgets = this._widgets.get();
|
||||
if (!widgets) { return; }
|
||||
|
||||
const searchText = this._searchText.get();
|
||||
if (!searchText) {
|
||||
this._filteredWidgets.set(widgets);
|
||||
} else {
|
||||
const searchTerms = searchText.trim().split(/\s+/);
|
||||
const searchPatterns = searchTerms.map(term =>
|
||||
new RegExp(`\\b${escapeRegExp(term)}`, 'i'));
|
||||
const filteredWidgets = widgets.filter(({cleanText}) =>
|
||||
searchPatterns.some(pattern => pattern.test(cleanText))
|
||||
);
|
||||
this._filteredWidgets.set(filteredWidgets);
|
||||
}
|
||||
}
|
||||
|
||||
private _buildWidgets() {
|
||||
return dom.domComputed(this._filteredWidgets, (widgets) => {
|
||||
if (widgets === null) {
|
||||
return cssLoadingSpinner(loadingSpinner());
|
||||
} else if (widgets.length === 0) {
|
||||
return cssNoMatchingWidgets(t('No matching widgets'));
|
||||
} else {
|
||||
return cssWidgets(
|
||||
widgets.map(widget => {
|
||||
const {description, authors = [], lastUpdatedAt} = widget;
|
||||
|
||||
return this._buildWidget({
|
||||
variant: getWidgetVariant(widget),
|
||||
id: getWidgetId(widget),
|
||||
name: getWidgetName(widget),
|
||||
description,
|
||||
developer: authors[0],
|
||||
lastUpdated: lastUpdatedAt,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _buildWidget(info: WidgetInfo) {
|
||||
const {variant, id, name, description, developer, lastUpdated} = info;
|
||||
|
||||
return cssWidget(
|
||||
dom.cls('custom-widget'),
|
||||
cssWidgetHeader(
|
||||
variant === 'custom' ? t('Add Your Own Widget') :
|
||||
variant === 'grist' ? t('Grist Widget') :
|
||||
withInfoTooltip(
|
||||
t('Community Widget'),
|
||||
'communityWidgets',
|
||||
{
|
||||
variant: 'hover',
|
||||
iconDomArgs: [cssTooltipIcon.cls('')],
|
||||
}
|
||||
),
|
||||
cssWidgetHeader.cls('-secondary', ['custom', 'community'].includes(variant)),
|
||||
),
|
||||
cssWidgetBody(
|
||||
cssWidgetName(
|
||||
name,
|
||||
testId('widget-name'),
|
||||
),
|
||||
cssWidgetDescription(
|
||||
description ?? t('(Missing info)'),
|
||||
cssWidgetDescription.cls('-missing', !description),
|
||||
testId('widget-description'),
|
||||
),
|
||||
variant === 'custom' ? null : cssWidgetMetadata(
|
||||
variant === 'grist' ? null : cssWidgetMetadataRow(
|
||||
cssWidgetMetadataName(t('Developer:')),
|
||||
cssWidgetMetadataValue(
|
||||
developer?.url
|
||||
? cssDeveloperLink(
|
||||
developer.name,
|
||||
{href: developer.url, target: '_blank'},
|
||||
dom.on('click', (ev) => ev.stopPropagation()),
|
||||
testId('widget-developer'),
|
||||
)
|
||||
: dom('span',
|
||||
developer?.name ?? t('(Missing info)'),
|
||||
testId('widget-developer'),
|
||||
),
|
||||
cssWidgetMetadataValue.cls('-missing', !developer?.name),
|
||||
testId('widget-developer'),
|
||||
),
|
||||
),
|
||||
cssWidgetMetadataRow(
|
||||
cssWidgetMetadataName(t('Last updated:')),
|
||||
cssWidgetMetadataValue(
|
||||
lastUpdated ?
|
||||
new Date(lastUpdated).toLocaleDateString('default', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
: t('(Missing info)'),
|
||||
cssWidgetMetadataValue.cls('-missing', !lastUpdated),
|
||||
testId('widget-last-updated'),
|
||||
),
|
||||
),
|
||||
testId('widget-metadata'),
|
||||
),
|
||||
variant !== 'custom' ? null : cssCustomUrlInput(
|
||||
this._customUrl,
|
||||
{placeholder: t('Widget URL')},
|
||||
testId('custom-url'),
|
||||
),
|
||||
),
|
||||
cssWidget.cls('-selected', use => id === use(this._selectedWidgetId)),
|
||||
dom.on('click', () => this._selectedWidgetId.set(id)),
|
||||
testId('widget'),
|
||||
testId(`widget-${variant}`),
|
||||
);
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
if (this._saveDisabled.get()) { return; }
|
||||
|
||||
await this._saveSelectedWidget();
|
||||
this._ctl.close();
|
||||
}
|
||||
|
||||
private async _deselectOrClose() {
|
||||
if (this._selectedWidgetId.get()) {
|
||||
this._selectedWidgetId.set(null);
|
||||
} else {
|
||||
this._ctl.close();
|
||||
}
|
||||
}
|
||||
|
||||
private async _saveSelectedWidget() {
|
||||
await this._gristDoc.docData.bundleActions(
|
||||
'Save selected custom widget',
|
||||
async () => {
|
||||
let section = this._section;
|
||||
if (!section) {
|
||||
const {addWidget} = this._options;
|
||||
if (!addWidget) {
|
||||
throw new Error('Cannot add custom widget: missing `addWidget` implementation');
|
||||
}
|
||||
|
||||
const {sectionRef} = await addWidget();
|
||||
const newSection = this._gristDoc.docModel.viewSections.getRowModel(sectionRef);
|
||||
if (!newSection.id.peek()) {
|
||||
throw new Error(`Section ${sectionRef} does not exist`);
|
||||
}
|
||||
section = newSection;
|
||||
}
|
||||
const selectedWidgetId = this._selectedWidgetId.get();
|
||||
if (selectedWidgetId === CUSTOM_URL_WIDGET_ID) {
|
||||
return this._saveCustomUrlWidget(section);
|
||||
} else {
|
||||
return this._saveRemoteWidget(section);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _saveCustomUrlWidget(section: ViewSectionRec) {
|
||||
bundleChanges(() => {
|
||||
section.customDef.renderAfterReady(false);
|
||||
section.customDef.url(this._customUrl.get());
|
||||
section.customDef.widgetId(null);
|
||||
section.customDef.widgetDef(null);
|
||||
section.customDef.pluginId('');
|
||||
section.customDef.access(AccessLevel.none);
|
||||
section.customDef.widgetOptions(null);
|
||||
section.hasCustomOptions(false);
|
||||
section.customDef.columnsMapping(null);
|
||||
section.columnsToMap(null);
|
||||
section.desiredAccessLevel(AccessLevel.none);
|
||||
});
|
||||
await section.saveCustomDef();
|
||||
}
|
||||
|
||||
private async _saveRemoteWidget(section: ViewSectionRec) {
|
||||
const [pluginId, widgetId] = this._selectedWidgetId.get()!.split(':');
|
||||
const {customDef} = section;
|
||||
if (customDef.pluginId.peek() === pluginId && customDef.widgetId.peek() === widgetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedWidget = matchWidget(this._widgets.get() ?? [], {widgetId, pluginId});
|
||||
if (!selectedWidget) {
|
||||
throw new Error(`Widget ${this._selectedWidgetId.get()} not found`);
|
||||
}
|
||||
|
||||
bundleChanges(() => {
|
||||
section.customDef.renderAfterReady(selectedWidget.renderAfterReady ?? false);
|
||||
section.customDef.access(AccessLevel.none);
|
||||
section.desiredAccessLevel(selectedWidget.accessLevel ?? AccessLevel.none);
|
||||
// Keep a record of the original widget definition.
|
||||
// Don't rely on this much, since the document could
|
||||
// have moved installation since, and widgets could be
|
||||
// served from elsewhere.
|
||||
section.customDef.widgetDef(selectedWidget);
|
||||
section.customDef.widgetId(selectedWidget.widgetId);
|
||||
section.customDef.pluginId(selectedWidget.source?.pluginId ?? '');
|
||||
section.customDef.url(null);
|
||||
section.customDef.widgetOptions(null);
|
||||
section.hasCustomOptions(false);
|
||||
section.customDef.columnsMapping(null);
|
||||
section.columnsToMap(null);
|
||||
});
|
||||
await section.saveCustomDef();
|
||||
}
|
||||
|
||||
private _maybeClearSelection(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (
|
||||
!target.closest('.custom-widget') &&
|
||||
!target.closest('button') &&
|
||||
!target.closest('a') &&
|
||||
!target.closest('input')
|
||||
) {
|
||||
this._selectedWidgetId.set(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getWidgetName({name, source}: ICustomWidget) {
|
||||
return source?.name ? `${name} (${source.name})` : name;
|
||||
}
|
||||
|
||||
function getWidgetVariant({isGristLabsMaintained = false, widgetId}: ICustomWidget): WidgetVariant {
|
||||
if (widgetId === CUSTOM_URL_WIDGET_ID) {
|
||||
return 'custom';
|
||||
} else if (isGristLabsMaintained) {
|
||||
return 'grist';
|
||||
} else {
|
||||
return 'community';
|
||||
}
|
||||
}
|
||||
|
||||
function getWidgetId({source, widgetId}: ICustomWidget) {
|
||||
if (widgetId === CUSTOM_URL_WIDGET_ID) {
|
||||
return CUSTOM_URL_WIDGET_ID;
|
||||
} else {
|
||||
return `${source?.pluginId ?? ''}:${widgetId}`;
|
||||
}
|
||||
}
|
||||
|
||||
function getWidgetCleanText({name, description, authors = []}: ICustomWidget) {
|
||||
let cleanText = name;
|
||||
if (description) { cleanText += ` ${description}`; }
|
||||
if (authors[0]) { cleanText += ` ${authors[0].name}`; }
|
||||
return cleanText;
|
||||
}
|
||||
|
||||
export const cssWidgetMetadata = styled('div', `
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 4px;
|
||||
`);
|
||||
|
||||
export const cssWidgetMetadataRow = styled('div', `
|
||||
display: flex;
|
||||
column-gap: 4px;
|
||||
`);
|
||||
|
||||
export const cssWidgetMetadataName = styled('span', `
|
||||
color: ${theme.lightText};
|
||||
font-weight: 600;
|
||||
`);
|
||||
|
||||
export const cssWidgetMetadataValue = styled('div', `
|
||||
&-missing {
|
||||
color: ${theme.lightText};
|
||||
}
|
||||
`);
|
||||
|
||||
export const cssDeveloperLink = styled(cssLink, `
|
||||
font-weight: 600;
|
||||
`);
|
||||
|
||||
const cssCustomWidgetGallery = styled('div', `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
`);
|
||||
|
||||
const WIDGET_WIDTH_PX = 240;
|
||||
|
||||
const WIDGETS_GAP_PX = 16;
|
||||
|
||||
const cssHeader = styled('div', `
|
||||
display: flex;
|
||||
column-gap: 16px;
|
||||
row-gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin: 40px 40px 16px 40px;
|
||||
|
||||
/* Don't go beyond the final grid column. */
|
||||
max-width: ${(3 * WIDGET_WIDTH_PX) + (2 * WIDGETS_GAP_PX)}px;
|
||||
`);
|
||||
|
||||
const cssTitle = styled('div', `
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
`);
|
||||
|
||||
const cssSearchInputWrapper = styled('div', `
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`);
|
||||
|
||||
const cssSearchIcon = styled(icon, `
|
||||
margin-left: 8px;
|
||||
position: absolute;
|
||||
--icon-color: ${theme.accentIcon};
|
||||
`);
|
||||
|
||||
const cssSearchInput = styled(textInput, `
|
||||
height: 28px;
|
||||
padding-left: 32px;
|
||||
`);
|
||||
|
||||
const cssShadowScroll = styled('div', `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: unset;
|
||||
flex-grow: 1;
|
||||
padding: 16px 40px;
|
||||
`);
|
||||
|
||||
const cssCenteredFlexGrow = styled('div', `
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`);
|
||||
|
||||
const cssLoadingSpinner = cssCenteredFlexGrow;
|
||||
|
||||
const cssNoMatchingWidgets = styled(cssCenteredFlexGrow, `
|
||||
color: ${theme.lightText};
|
||||
`);
|
||||
|
||||
const cssWidgets = styled('div', `
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(0px, ${WIDGET_WIDTH_PX}px));
|
||||
gap: ${WIDGETS_GAP_PX}px;
|
||||
`);
|
||||
|
||||
const cssWidget = styled('div', `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 1px 1px 4px 1px ${theme.widgetGalleryShadow};
|
||||
border-radius: 4px;
|
||||
min-height: 183.5px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: ${theme.widgetGalleryBgHover};
|
||||
}
|
||||
&-selected {
|
||||
outline: 2px solid ${theme.widgetGalleryBorderSelected};
|
||||
outline-offset: -2px;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssWidgetHeader = styled('div', `
|
||||
flex-shrink: 0;
|
||||
border: 2px solid ${theme.widgetGalleryBorder};
|
||||
border-bottom: 1px solid ${theme.widgetGalleryBorder};
|
||||
border-radius: 4px 4px 0px 0px;
|
||||
color: ${theme.lightText};
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
padding: 4px 18px;
|
||||
text-transform: uppercase;
|
||||
|
||||
&-secondary {
|
||||
border: 0px;
|
||||
color: ${theme.widgetGallerySecondaryHeaderFg};
|
||||
background-color: ${theme.widgetGallerySecondaryHeaderBg};
|
||||
}
|
||||
.${cssWidget.className}:hover &-secondary {
|
||||
background-color: ${theme.widgetGallerySecondaryHeaderBgHover};
|
||||
}
|
||||
`);
|
||||
|
||||
const cssWidgetBody = styled('div', `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
border: 2px solid ${theme.widgetGalleryBorder};
|
||||
border-top: 0px;
|
||||
border-radius: 0px 0px 4px 4px;
|
||||
padding: 16px;
|
||||
`);
|
||||
|
||||
const cssWidgetName = styled('div', `
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
`);
|
||||
|
||||
const cssWidgetDescription = styled('div', `
|
||||
margin-bottom: 24px;
|
||||
|
||||
&-missing {
|
||||
color: ${theme.lightText};
|
||||
}
|
||||
`);
|
||||
|
||||
const cssCustomUrlInput = styled(textInput, `
|
||||
height: 28px;
|
||||
`);
|
||||
|
||||
const cssHelpLink = styled(cssLink, `
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
column-gap: 8px;
|
||||
`);
|
||||
|
||||
const cssHelpIcon = styled(icon, `
|
||||
flex-shrink: 0;
|
||||
`);
|
||||
|
||||
const cssFooter = styled('div', `
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 40px;
|
||||
border-top: 1px solid ${theme.widgetGalleryBorder};
|
||||
`);
|
||||
|
||||
const cssFooterButtons = styled('div', `
|
||||
display: flex;
|
||||
column-gap: 8px;
|
||||
`);
|
||||
|
||||
const cssModal = styled('div', `
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 930px;
|
||||
max-height: 623px;
|
||||
padding: 0px;
|
||||
`);
|
||||
|
||||
const cssTooltipIcon = styled('div', `
|
||||
color: ${theme.widgetGallerySecondaryHeaderFg};
|
||||
border-color: ${theme.widgetGallerySecondaryHeaderFg};
|
||||
`);
|
After Width: | Height: | Size: 1.8 KiB |
Loading…
Reference in new issue