import { useState, useEffect, useCallback, memo } from '@wordpress/element'; import { Button, SelectControl, Disabled } from '@wordpress/components'; import apiFetch from '@wordpress/api-fetch'; import type { ResponseProps } from '@components/DesignPanel/types'; import { Notice, Spinner, TextControl, ToggleControl, } from '@wordpress/components'; import { TabsWrapper, TabsContent, TabsList } from '@components/ui/Tabs'; import has from 'lodash/has'; import { Modal, ModalClose, ModalContent, ModalFooter, ModalHeader, ModalTitle, ModalTrigger, } from '@components/ui/Modal'; import { DisappearingMessage } from '@components/ui/DisappearingMessage'; import { useSelect } from '@wordpress/data'; import PageIcon from 'blockbite-icons/dist/Pencil1'; import { Icon as IconComp } from '@components/ui/Icon'; export type DesignTokenProp = { token: string; value: string; name: string; optional?: any; }; export type DesignTokenProps = { fontSizes: any[]; colors: any[]; fonts: any[]; headings: any[]; }; export type DesignTokenOptinProps = { colors: boolean; fonts: boolean; fontSizes: boolean; headings: boolean; }; const FONTSIZESLOTS = 16; const COLORSSLOTS = 16; const FONTSSLOTS = 10; const HEADINGSLOTS = 7; export const createSlots = (themeValues: any) => { const { colors: colorValues = [], fonts: fontsValues = [], fontSizes: fontSizesValues = [], headings: headingsValues = [], } = themeValues; const createSlotsArray = ( count: number, tokenPrefix: string, names: string[] ) => { return Array.from({ length: count }, (_, i) => ({ token: `${tokenPrefix}-${i + 1}`, value: '', name: names.length ? names[i] : `${tokenPrefix}-${i + 1}`, optional: {}, })); }; // Generate slots for each category with specified lengths const fontSizes = createSlotsArray(FONTSIZESLOTS, 'b_fontsize', []); const colors = createSlotsArray(COLORSSLOTS, 'b_color', []); const fonts = createSlotsArray(FONTSSLOTS, 'b_font', []); const headings = createSlotsArray(HEADINGSLOTS, 'b_heading', [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', ]); const populateSlots = (slots: any[], data: any[]) => { (data || []).forEach((item, index) => { if (slots[index]) { slots[index].value = item.value || ''; slots[index].name = item.name || slots[index].name; if (item.optional) { slots[index].optional = { ...item.optional }; } } }); }; // Populate the slots with provided theme values populateSlots(fontSizes, fontSizesValues); populateSlots(colors, colorValues); populateSlots(fonts, fontsValues); populateSlots(headings, headingsValues); return { fontSizes, colors, fonts, headings }; }; export const saveDesignTokens = async ( themeOptin: DesignTokenOptinProps, inputFields: DesignTokenProps ) => { /* get native global styles global styles are written as a custom post type in the database if you change the site editor styles */ const nativeGlobalStyles = await apiFetch({ path: '/blockbite/v1/editor-settings/native-global-styles', }).then((res: any) => { return res; }); // get slots directly from native theme const updatedTheme: DesignTokenProps = { ...blockbite.getNativeTheme(nativeGlobalStyles), }; // check any optin is enabled and override them with enabled fields if (themeOptin.colors) { updatedTheme.colors = inputFields.colors; } if (themeOptin.fonts) { updatedTheme.fonts = inputFields.fonts; } if (themeOptin.fontSizes) { updatedTheme.fontSizes = inputFields.fontSizes; } if (themeOptin.headings) { updatedTheme.headings = inputFields.headings; } // fill remaining empty slots const slotTheme = createSlots(updatedTheme); // save current optin state apiFetch({ path: `/blockbite/v1/items/upsert-handle`, method: 'POST', data: { handle: 'design-tokens-optin', content: JSON.stringify(themeOptin), }, }); // save design tokens return apiFetch({ path: `/blockbite/v1/items/upsert-handle`, method: 'POST', data: { handle: 'design-tokens', platform: 'site', content: JSON.stringify(slotTheme), }, }).then((res: ResponseProps) => { if (res.status === 200) { // this will trigger to update the tailwind config wp.data.dispatch('biteStore/editor').setTheme(slotTheme); return res; } }); }; export const changeThemeOptin = (theme: DesignTokenOptinProps) => { apiFetch({ path: `/blockbite/v1/items/upsert-handle`, method: 'POST', data: { handle: 'design-tokens-optin', platform: 'site', content: JSON.stringify(theme), }, }); }; const FieldMapper = memo( ({ fieldType, inputFields, setInputFields, themeOptin, disableLabels = false, setMessage, }: any) => { const handleInputChange = useCallback( (token: any, key: any, value: any) => { setInputFields((prevFields: any) => ({ ...prevFields, [fieldType]: prevFields[fieldType].map((item: any) => item.token === token ? { ...item, [key]: value } : item ), })); }, [fieldType, setInputFields] ); return inputFields[fieldType].map((item: any) => (
{ if (!themeOptin[fieldType]) { setMessage('Please enable custom tokens to edit.'); } }} >
{!disableLabels && ( { if (themeOptin[fieldType]) { handleInputChange(item.token, 'name', e); } }} help={item.token} /> )} {fieldType === 'headings' ? (
({ value: item.token, label: item.name, }))} onChange={(token: string) => { if (themeOptin[fieldType]) { const fontFamilyByValue = inputFields.fontSizes.find( (fontFamily: any) => fontFamily.token === token ); handleInputChange( item.token, 'value', fontFamilyByValue.token ); } }} className="w-full" >
{ if (themeOptin[fieldType]) { handleInputChange(item.token, 'optional', { ...item.optional, lineHeight: e, }); } }} className="w-32 flex-none" />
({ value: item.token, label: item.name, }))} onChange={(token: string) => { if (themeOptin[fieldType]) { const fontFamilyByValue = inputFields.fonts.find( (fontFamily: any) => fontFamily.token === token ); handleInputChange(item.token, 'optional', { ...item.optional, font: fontFamilyByValue.token, }); } }} className="w-full" >
) : ( { if (themeOptin[fieldType]) { handleInputChange(item.token, 'value', e); } }} /> )}
)); } ); export const DesignTokenModal = () => { const [open, setOpen] = useState(false); const [message, setMessage] = useState(''); const [saving] = useState(false); const [inputFields, setInputFields] = useState({ colors: [] as any, fonts: [] as any, fontSizes: [] as any, headings: [] as any, }); const [themeOptin, setThemeOptin] = useState({ colors: false, fonts: false, fontSizes: false, headings: false, }); const themeInit = useSelect((select) => { // @ts-ignore return select('biteStore/editor').getThemeInit(); }, []); useEffect(() => { if (themeInit) { const getThemeOptin = wp.data.select('biteStore/editor').getThemeOptin(); const slots = createSlots(wp.data.select('biteStore/editor').getTheme()); setInputFields(slots); setThemeOptin(getThemeOptin); } }, [themeInit]); return ( { setOpen(isOpen); }} > {themeInit && ( Design Tokens {message && ( { setMessage(''); }} > {message} )}
{ setThemeOptin({ ...themeOptin, fontSizes: e }); }} label="Enable custom font size tokens" help="Customize the font sizes for your theme. Enabling custom font sizes will disable font size token sync." />
{ setThemeOptin({ ...themeOptin, colors: e }); }} label="Enable custom color tokens" help="Customize the colors for your theme. Enabling custom colors will disable color token sync." />
{ setThemeOptin({ ...themeOptin, headings: e }); }} label="Enable custom heading tokens" help="Customize the headings for your theme. Enabling custom headings will disable heading token sync." />
{ setThemeOptin({ ...themeOptin, fonts: e }); }} label="Enable custom font family tokens" help="Customize the font families for your theme. Enabling custom font families will disable font family token sync." />

Sync Design Tokens

Sync your design tokens from the{' '} Site Editor {' '} or theme.json to blockbite. Syncing will not affect custom design tokens.

{themeInit && ( )}
{saving && themeInit && } {saving && 'Syncing…'} {!saving && (
Synced!
)}
{themeInit && ( )}
)} {!themeInit && ( Design Tokens )}
); };