move devices

This commit is contained in:
Matt Batchelder
2026-05-12 23:13:47 -04:00
parent 088e1bdd06
commit 474fe7dfdc
4 changed files with 247 additions and 5 deletions

View File

@@ -1656,7 +1656,80 @@ p:last-child { margin-bottom: 0; }
font-size: .9rem;
}
/* ── 8d. Image Card ───────────────────────────────────────── */
/* ── 8d. Device Card ───────────────────────────────────────── */
.device-grid {
--cols: 2;
}
.oribi-card.device-card {
display: flex;
flex-direction: column;
border-top: 3px solid var(--color-primary);
background: var(--card-bg);
padding: 0;
overflow: hidden;
}
.device-card img,
.device-card .card-image {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
display: block;
}
.device-card-body {
display: flex;
flex-direction: column;
flex: 1;
padding: 1.5rem;
}
.device-best-for {
display: inline-block;
font-size: var(--wp--preset--font-size--xs, .75rem);
background: var(--color-primary-lt);
color: var(--color-primary);
padding: .25rem .75rem;
border-radius: 999px;
margin-bottom: .75rem;
font-weight: 600;
}
.device-card h3 {
color: var(--color-heading);
margin-bottom: .5rem;
}
.device-card p {
font-size: .9rem;
margin-bottom: 1rem;
}
.device-specs {
display: grid;
grid-template-columns: 1fr 1fr;
gap: .25rem .75rem;
font-size: .85rem;
margin: 0 0 1.25rem;
padding: 0;
}
.device-specs dt {
color: var(--color-muted, #6b7280);
font-weight: 500;
}
.device-specs dd {
margin: 0;
font-weight: 600;
color: var(--color-heading);
}
.device-price {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-primary);
margin-bottom: 1.25rem;
}
.device-card-btn {
display: block;
width: 100%;
text-align: center;
margin-top: auto;
}
/* ── 8e. Image Card ────────────────────────────────────────── */
.image-card {
padding: 0;
overflow: hidden;

View File

@@ -1007,6 +1007,86 @@
save: function () { return null; }
});
/* ── Device Card ──────────────────────────────────────────────────────── */
reg('oribi/device-card', {
title: 'Device Card',
icon: 'laptop',
category: 'oribi',
parent: ['oribi/device-section'],
supports: { html: false, reusable: false },
attributes: Object.assign({}, {
title: { type: 'string', default: '' },
description: { type: 'string', default: '' },
price: { type: 'string', default: '' },
bestFor: { type: 'string', default: '' },
specs: { type: 'array', default: [], items: { type: 'object' } },
btnText: { type: 'string', default: 'Order Now' },
btnUrl: { type: 'string', default: '/contact' },
}, CARD_IMAGE_ATTRS),
edit: function (props) {
var a = props.attributes, s = props.setAttributes;
var imgPrev = cardImagePreview(a);
function updateSpec(i, field, val) {
var next = (a.specs || []).slice();
next[i] = Object.assign({}, next[i], {});
next[i][field] = val;
s({ specs: next });
}
function addSpec() {
s({ specs: (a.specs || []).concat([{ key: '', value: '' }]) });
}
function removeSpec(i) {
var next = (a.specs || []).slice();
next.splice(i, 1);
s({ specs: next });
}
var specsRows = (a.specs || []).map(function (sp, i) {
return el('div', { key: i, style: { display: 'flex', gap: '8px', marginBottom: '6px' } },
el(TC, { label: 'Key', value: sp.key || '', onChange: function (v) { updateSpec(i, 'key', v); }, style: { flex: 1 } }),
el(TC, { label: 'Value', value: sp.value || '', onChange: function (v) { updateSpec(i, 'value', v); }, style: { flex: 1 } }),
el('button', { onClick: function () { removeSpec(i); }, style: { alignSelf: 'flex-end', marginBottom: '8px' }, className: 'button is-small is-destructive' }, '✕')
);
});
return el(Frag, null,
el(IC, null,
el(PB, { title: 'Card Content' },
el(TC, { label: 'Best For (pill)', value: a.bestFor || '', onChange: function (v) { s({ bestFor: v }); } }),
el(TC, { label: 'Price', value: a.price || '', onChange: function (v) { s({ price: v }); } }),
el(TC, { label: 'Button Text', value: a.btnText || '', onChange: function (v) { s({ btnText: v }); } }),
el(TC, { label: 'Button URL', value: a.btnUrl || '', onChange: function (v) { s({ btnUrl: v }); } })
),
el(PB, { title: 'Specs' },
specsRows,
el('button', { onClick: addSpec, className: 'button is-secondary', style: { marginTop: '4px' } }, '+ Add Spec')
),
cardImageControls(a, s)
),
el('div', { className: 'oribi-card device-card' },
imgPrev,
el('div', { className: 'device-card-body' },
a.bestFor ? el('span', { className: 'device-best-for' }, a.bestFor) : null,
el(RT, { tagName: 'h3', value: a.title, onChange: function (v) { s({ title: v }); }, placeholder: 'Device name...' }),
el(RT, { tagName: 'p', value: a.description, onChange: function (v) { s({ description: v }); }, placeholder: 'Short description...' }),
(a.specs || []).length ? el('dl', { className: 'device-specs' },
(a.specs || []).map(function (sp, i) {
return [
el('dt', { key: 'k' + i }, sp.key || ''),
el('dd', { key: 'v' + i }, sp.value || '')
];
})
) : null,
a.price ? el('div', { className: 'device-price' }, a.price) : null,
el('span', { className: 'wp-block-button__link device-card-btn' }, a.btnText || 'Order Now')
)
)
);
},
save: function () { return null; }
});
/* ── Image Card ───────────────────────────────────────────────────────── */
reg('oribi/image-card', {
title: 'Image Card',
@@ -1586,6 +1666,17 @@
save: function () { return el(IB.Content); }
});
/* DEVICE SECTION ───────────────────────────────────────────────────────── */
reg('oribi/device-section', {
title: 'Oribi Device Section',
icon: 'laptop',
category: 'oribi',
supports: { align: ['full'], html: false },
attributes: SECTION_ATTRS,
edit: createCardSectionEdit(['oribi/device-card'], [['oribi/device-card', {}]], 'Device Card'),
save: function () { return el(IB.Content); }
});
/* IMAGE SECTION ────────────────────────────────────────────────────────── */
reg('oribi/image-section', {
title: 'Oribi Image Section',

View File

@@ -455,6 +455,31 @@ add_action('init', function () {
'render_callback' => 'oribi_render_addon_card',
]);
/* Device Section (parent) */
register_block_type('oribi/device-section', [
'attributes' => oribi_card_section_attributes(2),
'supports' => $block_supports,
'render_callback' => 'oribi_render_device_section',
]);
/* Device Card (child) */
register_block_type('oribi/device-card', [
'attributes' => array_merge(
[
'title' => ['type' => 'string', 'default' => ''],
'description' => ['type' => 'string', 'default' => ''],
'price' => ['type' => 'string', 'default' => ''],
'bestFor' => ['type' => 'string', 'default' => ''],
'specs' => ['type' => 'array', 'default' => [], 'items' => ['type' => 'object']],
'btnText' => ['type' => 'string', 'default' => ''],
'btnUrl' => ['type' => 'string', 'default' => ''],
],
oribi_card_image_attributes()
),
'supports' => $block_supports,
'render_callback' => 'oribi_render_device_card',
]);
/* Image Section (parent) */
register_block_type('oribi/image-section', [
'attributes' => oribi_card_section_attributes(3),
@@ -1655,6 +1680,56 @@ function oribi_render_addon_card($a)
return ob_get_clean();
}
/* ── Device Section ────────────────────────────────────────────────────────── */
function oribi_render_device_section($a, $content)
{
return oribi_render_card_section($a, $content, 'grid device-grid', 2);
}
/* ── Device Card ───────────────────────────────────────────────────────────── */
function oribi_render_device_card($a)
{
$img = oribi_card_image_html($a);
$img_cls = $img['card_class'] ? ' ' . $img['card_class'] : '';
$specs = !empty($a['specs']) && is_array($a['specs']) ? $a['specs'] : [];
$price = !empty($a['price']) ? $a['price'] : '';
$bestFor = !empty($a['bestFor']) ? $a['bestFor'] : '';
$btnText = !empty($a['btnText']) ? $a['btnText'] : 'Order Now';
$btnUrl = !empty($a['btnUrl']) ? $a['btnUrl'] : '/contact';
ob_start(); ?>
<div class="oribi-card device-card<?php echo esc_attr($img_cls); ?>">
<?php if ($img['html']): echo $img['html']; endif; ?>
<div class="device-card-body">
<?php if ($bestFor): ?>
<span class="device-best-for"><?php echo esc_html($bestFor); ?></span>
<?php endif; ?>
<h3><?php echo wp_kses_post($a['title']); ?></h3>
<p><?php echo wp_kses_post($a['description']); ?></p>
<?php if ($specs): ?>
<dl class="device-specs">
<?php foreach ($specs as $spec):
$k = isset($spec['key']) ? $spec['key'] : '';
$v = isset($spec['value']) ? $spec['value'] : '';
if (!$k && !$v) continue; ?>
<dt><?php echo esc_html($k); ?></dt>
<dd><?php echo esc_html($v); ?></dd>
<?php endforeach; ?>
</dl>
<?php endif; ?>
<?php if ($price): ?>
<div class="device-price"><?php echo esc_html($price); ?></div>
<?php endif; ?>
<?php if ($btnText && $btnUrl): ?>
<a href="<?php echo esc_url($btnUrl); ?>" class="wp-block-button__link device-card-btn"><?php echo esc_html($btnText); ?></a>
<?php endif; ?>
</div>
</div>
<?php
return ob_get_clean();
}
/* ── Image Section ─────────────────────────────────────────────────────────── */
function oribi_render_image_section($a, $content)
{