feature: Initial commit

This commit is contained in:
Keith Solomon
2026-04-13 20:06:32 -05:00
commit 351dc06ec4
38 changed files with 8138 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
<?php
/**
* Block asset metadata.
*
* @package LogoSoup
*/
return array(
'dependencies' => array(
'wp-block-editor',
'wp-blocks',
'wp-components',
'wp-element',
'wp-i18n',
),
'version' => '0.1.0',
);
+60
View File
@@ -0,0 +1,60 @@
.logo-soup-editor {
border: 1px dashed #a7aaad;
padding: 16px;
}
.logo-soup-editor__toolbar {
margin-bottom: 16px;
}
.logo-soup-editor__list {
display: grid;
gap: 16px;
}
.logo-soup-editor__card {
background: #fff;
border: 1px solid #dcdcde;
border-radius: 10px;
padding: 16px;
}
.logo-soup-editor__thumb {
align-items: center;
background: linear-gradient(135deg, #f8f8f8, #ececec);
border-radius: 8px;
display: flex;
height: 92px;
justify-content: center;
margin-bottom: 12px;
padding: 12px;
}
.logo-soup-editor__thumb img {
max-height: var(--logo-soup-base-size, 52px);
max-width: 100%;
object-fit: contain;
}
.logo-soup-editor__preview.crop-to-content .logo-soup-editor__thumb {
padding: 4px;
}
.logo-soup-editor__preview.align-by-visual-center .logo-soup-editor__thumb img {
transform: translate(-2%, -2%);
}
.logo-soup-editor__preview.align-by-visual-center-x .logo-soup-editor__thumb img {
transform: translateX(-2%);
}
.logo-soup-editor__preview.align-by-visual-center-y .logo-soup-editor__thumb img {
transform: translateY(-2%);
}
.logo-soup-editor__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
+392
View File
@@ -0,0 +1,392 @@
(function (blocks, blockEditor, components, element, i18n) {
const registerBlockType = blocks.registerBlockType;
const InspectorControls = blockEditor.InspectorControls;
const MediaUpload = blockEditor.MediaUpload;
const MediaUploadCheck = blockEditor.MediaUploadCheck;
const useBlockProps = blockEditor.useBlockProps;
const Button = components.Button;
const PanelBody = components.PanelBody;
const Placeholder = components.Placeholder;
const RangeControl = components.RangeControl;
const SelectControl = components.SelectControl;
const TextControl = components.TextControl;
const ToggleControl = components.ToggleControl;
const Fragment = element.Fragment;
const el = element.createElement;
const __ = i18n.__;
function updateLogoAtIndex(logos, index, nextLogo) {
return logos.map(function (logo, logoIndex) {
return logoIndex === index ? nextLogo : logo;
});
}
registerBlockType('logo-soup/showcase', {
edit: function (props) {
const attributes = props.attributes;
const setAttributes = props.setAttributes;
const logos = attributes.logos || [];
const layout = attributes.layout || 'carousel';
const grayscale =
typeof attributes.grayscale === 'boolean' ? attributes.grayscale : true;
const speed = attributes.speed || 22;
const baseSize = attributes.baseSize || 48;
const scaleFactor =
typeof attributes.scaleFactor === 'number'
? attributes.scaleFactor
: 0.5;
const densityAware =
typeof attributes.densityAware === 'boolean'
? attributes.densityAware
: true;
const densityFactor =
typeof attributes.densityFactor === 'number'
? attributes.densityFactor
: 0.5;
const cropToContent =
typeof attributes.cropToContent === 'boolean'
? attributes.cropToContent
: false;
const alignBy = attributes.alignBy || 'bounds';
const pauseOnHover =
typeof attributes.pauseOnHover === 'boolean'
? attributes.pauseOnHover
: true;
const blockProps = useBlockProps({ className: 'logo-soup-editor' });
function addLogo(media) {
if (!media || !media.url) {
return;
}
setAttributes({
logos: logos.concat({
id: media.id || 0,
url: media.url,
alt: media.alt || media.title || '',
link: '',
}),
});
}
function removeLogo(index) {
setAttributes({
logos: logos.filter(function (_, logoIndex) {
return logoIndex !== index;
}),
});
}
function moveLogo(index, direction) {
const nextIndex = index + direction;
const nextLogos = logos.slice();
const currentLogo = nextLogos[index];
if (nextIndex < 0 || nextIndex >= logos.length) {
return;
}
nextLogos[index] = nextLogos[nextIndex];
nextLogos[nextIndex] = currentLogo;
setAttributes({ logos: nextLogos });
}
function renderMediaButton(label) {
return el(MediaUploadCheck, null, el(MediaUpload, {
onSelect: addLogo,
allowedTypes: ['image'],
render: function (mediaProps) {
return el(
Button,
{
variant: 'primary',
onClick: mediaProps.open,
},
label
);
},
}));
}
function renderLogoCard(logo, index) {
const cardKey = String(logo.id || 'logo') + '-' + String(index);
return el(
'div',
{
className: 'logo-soup-editor__card',
key: cardKey,
},
[
el(
'div',
{
className: 'logo-soup-editor__thumb',
key: 'thumb',
},
el('img', {
src: logo.url,
alt: logo.alt || '',
})
),
el(TextControl, {
key: 'alt',
label: __('Alt text', 'logo-soup'),
value: logo.alt || '',
onChange: function (value) {
setAttributes({
logos: updateLogoAtIndex(logos, index, {
id: logo.id,
url: logo.url,
alt: value,
link: logo.link,
}),
});
},
}),
el(TextControl, {
key: 'link',
label: __('Link URL', 'logo-soup'),
value: logo.link || '',
placeholder: 'https://example.com',
onChange: function (value) {
setAttributes({
logos: updateLogoAtIndex(logos, index, {
id: logo.id,
url: logo.url,
alt: logo.alt,
link: value,
}),
});
},
}),
el(
'div',
{
className: 'logo-soup-editor__actions',
key: 'actions',
},
[
el(
Button,
{
key: 'up',
disabled: index === 0,
onClick: function () {
moveLogo(index, -1);
},
},
__('Move Up', 'logo-soup')
),
el(
Button,
{
key: 'down',
disabled: index === logos.length - 1,
onClick: function () {
moveLogo(index, 1);
},
},
__('Move Down', 'logo-soup')
),
el(
Button,
{
key: 'remove',
isDestructive: true,
onClick: function () {
removeLogo(index);
},
},
__('Remove', 'logo-soup')
),
]
),
]
);
}
return el(Fragment, null, [
el(
InspectorControls,
{
key: 'controls',
},
el(
PanelBody,
{
title: __('Display', 'logo-soup'),
},
[
el(SelectControl, {
key: 'layout',
label: __('Layout', 'logo-soup'),
value: layout,
options: [
{ label: __('Carousel', 'logo-soup'), value: 'carousel' },
{ label: __('Grid', 'logo-soup'), value: 'grid' },
],
onChange: function (value) {
setAttributes({ layout: value });
},
}),
el(ToggleControl, {
key: 'grayscale',
label: __('Use grayscale styling', 'logo-soup'),
checked: grayscale,
onChange: function (value) {
setAttributes({ grayscale: value });
},
}),
el(ToggleControl, {
key: 'pauseOnHover',
label: __('Pause carousel on hover', 'logo-soup'),
checked: pauseOnHover,
help: __('Applies to carousel layout only.', 'logo-soup'),
onChange: function (value) {
setAttributes({ pauseOnHover: value });
},
}),
el(RangeControl, {
key: 'speed',
label: __('Carousel speed (seconds)', 'logo-soup'),
value: speed,
min: 8,
max: 80,
onChange: function (value) {
setAttributes({ speed: value });
},
}),
el(RangeControl, {
key: 'baseSize',
label: __('Base size', 'logo-soup'),
value: baseSize,
min: 16,
max: 160,
onChange: function (value) {
setAttributes({ baseSize: value });
},
}),
el(RangeControl, {
key: 'scaleFactor',
label: __('Scale factor', 'logo-soup'),
value: scaleFactor,
min: 0.1,
max: 2,
step: 0.1,
onChange: function (value) {
setAttributes({ scaleFactor: value });
},
}),
el(ToggleControl, {
key: 'densityAware',
label: __('Density aware', 'logo-soup'),
checked: densityAware,
onChange: function (value) {
setAttributes({ densityAware: value });
},
}),
el(RangeControl, {
key: 'densityFactor',
label: __('Density factor', 'logo-soup'),
value: densityFactor,
min: 0,
max: 1,
step: 0.1,
onChange: function (value) {
setAttributes({ densityFactor: value });
},
}),
el(ToggleControl, {
key: 'cropToContent',
label: __('Crop to content', 'logo-soup'),
checked: cropToContent,
onChange: function (value) {
setAttributes({ cropToContent: value });
},
}),
el(SelectControl, {
key: 'alignBy',
label: __('Align by', 'logo-soup'),
value: alignBy,
options: [
{ label: __('Bounds', 'logo-soup'), value: 'bounds' },
{
label: __('Visual center', 'logo-soup'),
value: 'visual-center',
},
{
label: __('Visual center X', 'logo-soup'),
value: 'visual-center-x',
},
{
label: __('Visual center Y', 'logo-soup'),
value: 'visual-center-y',
},
],
onChange: function (value) {
setAttributes({ alignBy: value });
},
}),
]
)
),
el(
'div',
blockProps,
logos.length === 0
? el(
Placeholder,
{
label: __('Logo Soup', 'logo-soup'),
instructions: __(
'Upload a set of logos and optional links.',
'logo-soup'
),
},
renderMediaButton(__('Add Logo', 'logo-soup'))
)
: el(
'div',
{
className:
'logo-soup-editor__preview layout-' +
layout +
(grayscale ? ' is-grayscale' : '') +
(cropToContent ? ' crop-to-content' : '') +
(densityAware ? ' is-density-aware' : '') +
' align-by-' +
alignBy,
style: {
'--logo-soup-base-size': String(baseSize) + 'px',
'--logo-soup-scale-factor': String(scaleFactor),
'--logo-soup-density-factor': String(densityFactor),
},
},
[
el(
'div',
{
className: 'logo-soup-editor__toolbar',
key: 'toolbar',
},
renderMediaButton(__('Add Another Logo', 'logo-soup'))
),
el(
'div',
{
className: 'logo-soup-editor__list',
key: 'list',
},
logos.map(renderLogoCard)
),
]
)
),
]);
},
save: function () {
return null;
},
});
}(window.wp.blocks, window.wp.blockEditor, window.wp.components, window.wp.element, window.wp.i18n));
+116
View File
@@ -0,0 +1,116 @@
.logo-soup {
--logo-soup-gap: clamp(1rem, 2vw, 2rem);
--logo-soup-base-size: 48px;
--logo-soup-effective-size: var(--logo-soup-base-size);
--logo-soup-gap-factor: 1;
--logo-soup-width-factor: 3.5;
--logo-soup-gray-opacity: 0.72;
overflow: hidden;
position: relative;
width: 100%;
}
.logo-soup__track {
align-items: center;
display: flex;
gap: calc(var(--logo-soup-gap) * var(--logo-soup-gap-factor));
width: max-content;
}
.logo-soup.layout-carousel .logo-soup__track {
animation: logo-soup-scroll var(--logo-soup-speed, 22s) linear infinite;
}
.logo-soup.layout-grid .logo-soup__track {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(100%, calc(var(--logo-soup-effective-size) * 2.4)), 1fr)
);
justify-items: center;
width: 100%;
}
.logo-soup.pause-on-hover:hover .logo-soup__track {
animation-play-state: paused;
}
.logo-soup__item {
align-items: center;
display: inline-flex;
flex: 0 0 auto;
justify-content: center;
min-height: calc(var(--logo-soup-effective-size) + 1.5rem);
padding: 0.75rem 1rem;
text-decoration: none;
}
.logo-soup.layout-grid .logo-soup__item {
padding-inline: calc(var(--logo-soup-gap) * 0.25);
width: 100%;
}
.logo-soup__image {
display: block;
height: var(--logo-soup-effective-size);
max-width: min(calc(var(--logo-soup-effective-size) * var(--logo-soup-width-factor)), 22vw);
object-fit: contain;
opacity: 0.92;
transform: translate3d(0, 0, 0);
transition: filter 0.2s ease, opacity 0.2s ease, transform 0.2s ease;
width: auto;
}
.logo-soup.layout-grid .logo-soup__image {
margin-inline: auto;
max-width: min(100%, calc(var(--logo-soup-effective-size) * var(--logo-soup-width-factor)));
}
.logo-soup.crop-to-content .logo-soup__item {
padding: 0.5rem;
}
.logo-soup.align-by-visual-center .logo-soup__image {
transform: translate(-2%, -2%);
}
.logo-soup.align-by-visual-center-x .logo-soup__image {
transform: translateX(-2%);
}
.logo-soup.align-by-visual-center-y .logo-soup__image {
transform: translateY(-2%);
}
.logo-soup.is-grayscale .logo-soup__image {
filter: grayscale(100%);
opacity: var(--logo-soup-gray-opacity);
}
.logo-soup__item:hover .logo-soup__image,
.logo-soup__item:focus .logo-soup__image {
filter: grayscale(0%);
opacity: 1;
transform: translateY(-1px);
}
@keyframes logo-soup-scroll {
from {
transform: translateX(0%);
}
to {
transform: translateX(calc(-50% - (var(--logo-soup-gap) / 2)));
}
}
@media (max-width: 781px) {
.logo-soup__item {
min-height: calc(var(--logo-soup-effective-size) + 1rem);
padding: 0.5rem 0.75rem;
}
.logo-soup__image {
max-width: min(calc(var(--logo-soup-effective-size) * 2.5), 34vw);
}
}