feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run

- Base: wintercms/winter branch 1.2 (full framework)
- Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS
- Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication
- Partials: hero (offline-first), features, modes (offline/nube toggle),
  screenshots, pricing (3 planes), comparison, FAQ, CTA
- Plugin VivesPOS.Site with ContactForm
- Dockerfile: PHP 8.2 Apache, port 80, healthcheck
- Added winter/wn-pages, blog, sitemap, seo plugins
- Active theme set to vivespos
This commit is contained in:
2026-08-21 19:29:00 -06:00
commit 1f72193a64
3266 changed files with 531480 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
.field-repeater{padding-top:5px}
.field-repeater .field-repeater-items{counter-reset:repeater-index-counter}
.field-repeater ul.field-repeater-items,
.field-repeater li.field-repeater-item{padding:0;margin:0;list-style:none}
.field-repeater ul.field-repeater-items>li.dragged{opacity:0.7;position:absolute;padding-top:15px;padding-right:15px;z-index:2000;background-color:#f9f9f9;border:1px dashed #dbdee0}
.field-repeater ul.field-repeater-items>li.dragged .repeater-item-remove{opacity:0}
.field-repeater ul.field-repeater-items>li.dragged .repeater-item-collapsed-title{top:5px}
.field-repeater ul.field-repeater-items>li.placeholder{display:block;position:relative;height:25px;margin-bottom:5px}
.field-repeater ul.field-repeater-items>li.placeholder:before{display:block;position:absolute;font-family:"Font Awesome 6 Free";font-weight:900;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-style:normal;font-variant:normal;text-rendering:auto;content:"\f054";color:#d35714;left:-10px;top:8px;z-index:2000}
.field-repeater li.field-repeater-item{position:relative;margin:0 0 1em 1em !important;padding:1.5em 1.25em 0 1.25em !important;background:#f5f5f5;border:1px solid #d1d6d9;border-radius:3px;box-shadow:inset 0 1px 0 rgba(209,214,217,0.25),0 1px 0 rgba(255,255,255,.5);min-height:30px}
.field-repeater li.field-repeater-item:before{color:#bdc3c7;font-family:"Font Awesome 6 Free";font-weight:900;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-style:normal;font-variant:normal;text-rendering:auto;content:"\f111";font-size:8px;position:absolute;left:-18px;top:-2px}
.field-repeater li.field-repeater-item:after{counter-increment:repeater-index-counter;content:counter(repeater-index-counter);display:block;position:absolute;font-size:12px;left:-27px;color:#bdc3c7;top:10px;width:24px;text-align:center}
.field-repeater li.field-repeater-item.collapsed .field-repeater-form{display:none}
.field-repeater li.field-repeater-item.collapsed .repeater-item-collapse .repeater-item-collapse-one{-webkit-transform:scale(1,-1);-ms-transform:scale(1,-1);transform:scale(1,-1)}
.field-repeater li.field-repeater-item.collapsed .repeater-item-collapsed-title{display:block}
.field-repeater li.field-repeater-item .repeater-item-collapsed-title{display:none;position:absolute;font-size:13px;top:50%;left:10px;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%)}
.field-repeater li.field-repeater-item .field-repeater-form{position:relative;top:-7px}
.field-repeater li.field-repeater-item .field-repeater-form:before,
.field-repeater li.field-repeater-item .field-repeater-form:after{content:" ";display:table}
.field-repeater li.field-repeater-item .field-repeater-form:after{clear:both}
.field-repeater li.field-repeater-item .repeater-item-handle{position:absolute;top:-6px;left:-25px;color:#bdc3c7;background:#f9f9f9;cursor:move;opacity:0;border-radius:999px;-webkit-transition:opacity 0.5s;transition:opacity 0.5s}
.field-repeater li.field-repeater-item .repeater-item-handle:hover{color:#999}
.field-repeater li.field-repeater-item .repeater-item-collapse{position:absolute;top:5px;right:30px;z-index:90}
.field-repeater li.field-repeater-item .repeater-item-collapse a,
.field-repeater li.field-repeater-item .repeater-item-collapse button{-webkit-transition:transform 0.3s;transition:transform 0.3s;color:#bdc3c7;line-height:20px;display:block;font-size:12px}
.field-repeater li.field-repeater-item .repeater-item-collapse a:hover,
.field-repeater li.field-repeater-item .repeater-item-collapse button:hover,
.field-repeater li.field-repeater-item .repeater-item-collapse a:focus,
.field-repeater li.field-repeater-item .repeater-item-collapse button:focus{color:#999;text-decoration:none}
.field-repeater li.field-repeater-item .repeater-item-remove{position:absolute;top:4px;right:5px;z-index:90}
.field-repeater li.field-repeater-item .repeater-item-remove.disabled{display:none}
.field-repeater li.field-repeater-item .repeater-item-remove.disabled + .repeater-item-collapse{right:7px}
.field-repeater li.field-repeater-item .repeater-item-remove .close{float:none;display:inline-block}
.field-repeater li.field-repeater-item .repeater-item-collapse,
.field-repeater li.field-repeater-item .repeater-item-handle,
.field-repeater li.field-repeater-item .repeater-item-remove{width:20px;height:20px;text-align:center}
.field-repeater li.field-repeater-item:hover .repeater-item-collapse,
.field-repeater li.field-repeater-item:active .repeater-item-collapse,
.field-repeater li.field-repeater-item:hover .repeater-item-handle,
.field-repeater li.field-repeater-item:active .repeater-item-handle,
.field-repeater li.field-repeater-item:hover .repeater-item-remove,
.field-repeater li.field-repeater-item:active .repeater-item-remove{opacity:1}
.field-repeater .field-repeater-add-item{position:relative;margin-top:10px;margin-left:20px;border:2px dotted #e0e0e0;border-radius:5px}
.field-repeater .field-repeater-add-item:before{color:#bdc3c7;font-family:"Font Awesome 6 Free";font-weight:900;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-style:normal;font-variant:normal;text-rendering:auto;content:"\2b";font-size:16px;position:absolute;left:-23px;top:-11px}
.field-repeater .field-repeater-add-item>a{color:#bdc3c7;text-align:center;display:block;text-decoration:none;padding:13px 15px;text-transform:uppercase;font-weight:600;font-size:12px}
.field-repeater .field-repeater-add-item:hover,
.field-repeater .field-repeater-add-item:focus{background-color:#48b2ce;border-color:transparent}
.field-repeater .field-repeater-add-item:hover:before,
.field-repeater .field-repeater-add-item:focus:before{color:#999}
.field-repeater .field-repeater-add-item:hover>a,
.field-repeater .field-repeater-add-item:focus>a{color:#fff}
.field-repeater .field-repeater-add-item:active{background:#6cc551;border-color:transparent}
.field-repeater .field-repeater-add-item:active>a{color:#fff}
.field-repeater .field-repeater-add-item.in-progress{border-color:#e0e0e0 !important;background:transparent !important}
.field-repeater[data-mode="grid"]>ul.field-repeater-items{display:grid;gap:20px}
.field-repeater[data-mode="grid"]>ul.field-repeater-items>.field-repeater-item{margin-bottom:0 !important}
.field-repeater[data-mode="grid"]>ul.field-repeater-items>.field-repeater-add-item{margin-top:0}
.field-repeater[data-mode="grid"]>ul.field-repeater-items>.field-repeater-add-item>a{display:flex;flex-direction:column;justify-content:center;height:100%}
.field-repeater[data-mode="grid"]>ul.field-repeater-items>.field-repeater-add-item:before{display:none}
.field-repeater[data-mode="grid"][data-columns="2"] ul.field-repeater-items{grid-template-columns:repeat(2,1fr)}
.field-repeater[data-mode="grid"][data-columns="3"] ul.field-repeater-items{grid-template-columns:repeat(3,1fr)}
.field-repeater[data-mode="grid"][data-columns="4"] ul.field-repeater-items{grid-template-columns:repeat(4,1fr)}
@media (max-width:1600px){.field-repeater[data-mode="grid"][data-columns="4"] ul.field-repeater-items{grid-template-columns:repeat(3,1fr)}}
.field-repeater[data-mode="grid"][data-columns="5"] ul.field-repeater-items{grid-template-columns:repeat(5,1fr)}
@media (max-width:1600px){.field-repeater[data-mode="grid"][data-columns="5"] ul.field-repeater-items{grid-template-columns:repeat(4,1fr)}}
.field-repeater[data-mode="grid"][data-columns="6"] ul.field-repeater-items{grid-template-columns:repeat(6,1fr)}
@media (max-width:1600px){.field-repeater[data-mode="grid"][data-columns="6"] ul.field-repeater-items{grid-template-columns:repeat(4,1fr)}}
@media (min-width:768px) and (max-width:1199px){.field-repeater[data-mode="grid"]>ul.field-repeater-items{grid-template-columns:repeat(2,1fr) !important}}
@media (max-width:767px){.field-repeater[data-mode="grid"]>ul.field-repeater-items{grid-template-columns:1fr !important}.field-repeater[data-mode="grid"]>ul.field-repeater-items>.field-repeater-item,.field-repeater[data-mode="grid"]>ul.field-repeater-items>.field-repeater-add-item{min-height:0 !important}.field-repeater[data-mode="grid"]>ul.field-repeater-items>.field-repeater-add-item{margin-top:10px}.field-repeater[data-mode="grid"]>ul.field-repeater-items>.field-repeater-add-item::before{display:block}}
.repeater-group-search-container{padding:10px 15px;border-bottom:1px solid #e0e0e0}
.repeater-group-search-container>div{position:relative}
.repeater-group-search-container .icon-search{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#999;pointer-events:none;z-index:10}
.repeater-group-search-container .repeater-group-search{padding-left:32px;padding-right:32px;width:100%}
.repeater-group-search-container .repeater-group-search-clear{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:transparent;border:none;padding:4px 6px;cursor:pointer;display:none;z-index:10;color:#999}
.repeater-group-search-container .repeater-group-search-clear i{font-size:14px}
.repeater-group-search-container .repeater-group-search-clear:hover{color:#666}
.repeater-group-no-results{display:none;padding:20px;text-align:center;color:#999}
.repeater-group-items-container{min-width:500px;max-width:800px}
.repeater-group-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:8px;padding:15px}
.repeater-group-grid .repeater-group-item a{display:flex;height:100%;align-items:center;gap:16px;padding:12px 12px 12px 16px;border:1px solid #e0e0e0;border-radius:3px;text-decoration:none;color:inherit;transition:all 0.2s}
.repeater-group-grid .repeater-group-item a i{width:1em;text-align:center;font-size:24px;color:#666;margin-top:2px;flex-shrink:0}
.repeater-group-grid .repeater-group-item a>div{flex:1;min-width:0}
.repeater-group-grid .repeater-group-item a>div .title{font-weight:600;font-size:13px;display:block;margin-bottom:2px}
.repeater-group-grid .repeater-group-item a>div .description{font-size:11px;color:#999;display:block;line-height:1.3}
.repeater-group-grid .repeater-group-item a:hover{border-color:#2da7c7;box-shadow:0 2px 8px rgba(0,0,0,0.1);transform:translateY(-2px)}

View File

@@ -0,0 +1,424 @@
/*
* Field Repeater plugin
*
* Data attributes:
* - data-control="fieldrepeater" - enables the plugin on an element
* - data-option="value" - an option with a value
*
* JavaScript API:
* $('a#someElement').fieldRepeater({...})
*/
+function ($) { "use strict";
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
// FIELD REPEATER CLASS DEFINITION
// ============================
var Repeater = function(element, options) {
this.options = options
this.$el = $(element)
if (this.options.sortable) {
this.$sortable = $(options.sortableContainer, this.$el)
}
$.wn.foundation.controlUtils.markDisposable(element)
Base.call(this)
this.init()
}
Repeater.prototype = Object.create(BaseProto)
Repeater.prototype.constructor = Repeater
Repeater.DEFAULTS = {
sortableHandle: '.repeater-item-handle',
sortableContainer: 'ul.field-repeater-items',
titleFrom: null,
minItems: null,
maxItems: null,
sortable: false,
mode: 'list',
style: 'default',
}
Repeater.prototype.init = function() {
if (this.options.sortable) {
this.bindSorting()
}
this.$el.on('ajaxDone', '> .field-repeater-items > .field-repeater-item > .repeater-item-remove > [data-repeater-remove]', this.proxy(this.onRemoveItemSuccess))
this.$el.on('ajaxDone', '> .field-repeater-items > .field-repeater-add-item > [data-repeater-add]', this.proxy(this.onAddItemSuccess))
this.$el.on('click', '> ul > li > .repeater-item-collapse .repeater-item-collapse-one', this.proxy(this.toggleCollapse))
this.$el.on('click', '> .field-repeater-items > .field-repeater-add-item > [data-repeater-add-group]', this.proxy(this.clickAddGroupButton))
this.$el.find('> ul > li > .repeater-item-collapsed-title')
.css({'cursor': 'pointer', 'width': '100%'})
.on('click', this.proxy(this.toggleCollapse))
this.$el.find('> ul > li > .field-repeater-form > .form-group > label')
.css({'cursor': 'pointer', 'width': '100%'})
.on('click', this.proxy(this.toggleCollapse))
this.$el.one('dispose-control', this.proxy(this.dispose))
this.togglePrompt()
this.applyStyle()
}
Repeater.prototype.dispose = function() {
if (this.options.sortable) {
this.$sortable.sortable('destroy')
}
this.$el.off('ajaxDone', '> .field-repeater-items > .field-repeater-item > .repeater-item-remove > [data-repeater-remove]', this.proxy(this.onRemoveItemSuccess))
this.$el.off('ajaxDone', '> .field-repeater-items > .field-repeater-add-item > [data-repeater-add]', this.proxy(this.onAddItemSuccess))
this.$el.off('click', '> ul > li > .repeater-item-collapse .repeater-item-collapse-one', this.proxy(this.toggleCollapse))
this.$el.off('click', '> ul > li > .repeater-item-collapsed-title', this.proxy(this.toggleCollapse))
this.$el.off('click', '> ul > li > .field-repeater-form > .form-group > label', this.proxy(this.toggleCollapse))
this.$el.off('click', '> .field-repeater-items > .field-repeater-add-item > [data-repeater-add-group]', this.proxy(this.clickAddGroupButton))
this.$el.off('dispose-control', this.proxy(this.dispose))
this.$el.removeData('oc.repeater')
this.$el = null
this.$sortable = null
this.options = null
BaseProto.dispose.call(this)
}
// Deprecated
Repeater.prototype.unbind = function() {
this.dispose()
}
Repeater.prototype.bindSorting = function() {
var sortableOptions = {
handle: this.options.sortableHandle,
nested: false,
vertical: this.options.mode === 'list',
}
this.$sortable.sortable(sortableOptions)
}
Repeater.prototype.clickAddGroupButton = function(ev) {
var $self = this
var templateHtml = $('> [data-group-palette-template]', this.$el).html(),
$target = $(ev.target),
$form = this.$el.closest('form'),
$loadContainer = $target.closest('.loading-indicator-container')
$target.ocPopover({
content: templateHtml
})
var $container = $target.data('oc.popover').$container
// Initialize the scrollpad control in the popup
$container.trigger('render')
$container
.on('click', 'a', function (ev) {
setTimeout(function() { $(ev.target).trigger('close.oc.popover') }, 1)
})
.on('ajaxPromise', '[data-repeater-add]', function(ev, context) {
$loadContainer.loadIndicator()
$(window).one('ajaxUpdateComplete', function() {
$loadContainer.loadIndicator('hide')
$self.togglePrompt()
$($self.$el).find('.field-repeater-items > .field-repeater-add-item').each(function () {
if ($(this).children().length === 0) {
$(this).remove()
}
})
})
})
$('[data-repeater-add]', $container).data('request-form', $form)
// Setup search functionality
var $searchInput = $('.repeater-group-search', $container)
var $clearButton = $('.repeater-group-search-clear', $container)
var $noResults = $('.repeater-group-no-results', $container)
var $itemsContainer = $('.repeater-group-items-container', $container)
var $listItems = $('.repeater-group-item', $container)
// Add hover effects to grid items
$listItems.on('mouseenter', 'a', function() {
$(this).css({
'border-color': '#3498db',
'box-shadow': '0 2px 8px rgba(0,0,0,0.1)',
'transform': 'translateY(-2px)'
})
}).on('mouseleave', 'a', function() {
$(this).css({
'border-color': '#e0e0e0',
'box-shadow': 'none',
'transform': 'translateY(0)'
})
})
$searchInput.on('input', function() {
var searchTerm = $(this).val().toLowerCase().trim()
// Show/hide clear button
$clearButton.toggle(searchTerm.length > 0)
if (searchTerm === '') {
// Show all items
$listItems.show()
$noResults.hide()
$itemsContainer.show()
return
}
var visibleCount = 0
// Filter items
$listItems.each(function() {
var $item = $(this)
var title = $('.title', $item).text().toLowerCase()
var description = $('.description', $item).text().toLowerCase()
if (title.indexOf(searchTerm) !== -1 || description.indexOf(searchTerm) !== -1) {
$item.show()
visibleCount++
} else {
$item.hide()
}
})
// Show/hide no results message and adjust container height
if (visibleCount === 0) {
$noResults.show()
$itemsContainer.hide()
} else {
$noResults.hide()
$itemsContainer.show()
}
})
// Clear search functionality
$clearButton.on('click', function() {
$searchInput.val('').trigger('input').focus()
})
// Focus search input when popover opens
setTimeout(function() {
$searchInput.focus()
}, 100)
}
Repeater.prototype.onRemoveItemSuccess = function(ev) {
var $target = $(ev.target)
// Allow any widgets inside a deleted item to be disposed
$target.closest('.field-repeater-item').find('[data-disposable]').each(function () {
var $elem = $(this),
control = $elem.data('control'),
widget = $elem.data('oc.' + control)
if (widget && typeof widget['dispose'] === 'function') {
widget.dispose()
}
})
$target.closest('[data-field-name]').trigger('change.oc.formwidget')
$target.closest('.field-repeater-item').remove()
this.togglePrompt()
}
Repeater.prototype.onAddItemSuccess = function(ev) {
window.requestAnimationFrame(() => {
this.togglePrompt()
$(ev.target).closest('[data-field-name]').trigger('change.oc.formwidget')
$(this.$el).find('.field-repeater-items > .field-repeater-add-item').each(function () {
if ($(this).children().length === 0) {
$(this).remove()
}
})
})
}
Repeater.prototype.togglePrompt = function () {
if (this.options.minItems && this.options.minItems > 0) {
var repeatedItems = this.$el.find('> .field-repeater-items > .field-repeater-item').length,
$removeItemBtn = this.$el.find('> .field-repeater-items > .field-repeater-item > .repeater-item-remove')
$removeItemBtn.toggleClass('disabled', !(repeatedItems > this.options.minItems))
}
if (this.options.maxItems && this.options.maxItems > 0) {
var repeatedItems = this.$el.find('> .field-repeater-items > .field-repeater-item').length,
$addItemBtn = this.$el.find('> .field-repeater-items > .field-repeater-add-item')
$addItemBtn.toggle(repeatedItems < this.options.maxItems)
}
}
Repeater.prototype.toggleCollapse = function(ev) {
var $item = $(ev.target).closest('.field-repeater-item'),
isCollapsed = $item.hasClass('collapsed')
ev.preventDefault()
if (this.getStyle() === 'accordion') {
if (isCollapsed) {
this.expand($item)
}
return
}
if (ev.ctrlKey || ev.metaKey) {
isCollapsed ? this.expandAll() : this.collapseAll()
}
else {
isCollapsed ? this.expand($item) : this.collapse($item)
}
}
Repeater.prototype.collapseAll = function() {
var self = this,
items = $(this.$el).children('.field-repeater-items').children('.field-repeater-item')
$.each(items, function(key, item){
self.collapse($(item))
})
}
Repeater.prototype.expandAll = function() {
var self = this,
items = $(this.$el).children('.field-repeater-items').children('.field-repeater-item')
$.each(items, function(key, item){
self.expand($(item))
})
}
Repeater.prototype.collapse = function($item) {
$item.addClass('collapsed')
$('.repeater-item-collapsed-title', $item).text(this.getCollapseTitle($item))
}
Repeater.prototype.expand = function($item) {
if (this.getStyle() === 'accordion') {
this.collapseAll()
}
$item.removeClass('collapsed')
}
Repeater.prototype.getCollapseTitle = function($item) {
var $target,
defaultText = '',
explicitText = $item.data('collapse-title'),
selector = 'input[type=text], select:first, ul:first'
if (explicitText) {
return explicitText
}
if (this.options.titleFrom) {
$target = $('[data-field-name="'+this.options.titleFrom+'"]', $item)
if ($target.length) {
selector = 'input, select:first, ul:first'
}
}
if (!$target || !$target.length) {
$target = $item
}
var $textInput = $(selector, $target).first()
if ($textInput.length) {
switch($textInput.prop("tagName")) {
case 'SELECT':
return $textInput.find('option:selected').text()
case 'UL':
return $textInput.find('li.active').text()
default:
return $textInput.val()
}
} else {
var $disabledTextInput = $('.form-control:first', $target)
if ($disabledTextInput.length) {
return $disabledTextInput.text()
}
}
return defaultText
}
Repeater.prototype.getStyle = function() {
var style = 'default'
// Validate style
if (this.options.style && ['collapsed', 'accordion'].indexOf(this.options.style) !== -1) {
style = this.options.style
}
return style
}
Repeater.prototype.applyStyle = function() {
if (this.options.mode === 'grid') {
return
}
var style = this.getStyle(),
self = this,
items = $(this.$el).children('.field-repeater-items').children('.field-repeater-item')
$.each(items, function(key, item) {
switch (style) {
case 'collapsed':
self.collapse($(item))
break
case 'accordion':
if (key !== 0) {
self.collapse($(item))
}
break
}
})
}
// FIELD REPEATER PLUGIN DEFINITION
// ============================
var old = $.fn.fieldRepeater
$.fn.fieldRepeater = function (option) {
var args = Array.prototype.slice.call(arguments, 1), result
this.each(function () {
var $this = $(this)
var data = $this.data('oc.repeater')
var options = $.extend({}, Repeater.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.repeater', (data = new Repeater(this, options)))
if (typeof option == 'string') result = data[option].apply(data, args)
if (typeof result != 'undefined') return false
})
return result ? result : this
}
$.fn.fieldRepeater.Constructor = Repeater
// FIELD REPEATER NO CONFLICT
// =================
$.fn.fieldRepeater.noConflict = function () {
$.fn.fieldRepeater = old
return this
}
// FIELD REPEATER DATA-API
// ===============
$(document).render(function() {
$('[data-control="fieldrepeater"]').fieldRepeater()
})
}(window.jQuery);

View File

@@ -0,0 +1,442 @@
// out: false
@import "../../../../assets/less/core/boot.less";
.field-repeater {
padding-top: 5px;
.field-repeater-items {
counter-reset: repeater-index-counter;
}
ul.field-repeater-items,
li.field-repeater-item {
padding: 0;
margin: 0;
list-style: none;
}
ul.field-repeater-items > li {
&.dragged {
opacity: .7;
position: absolute;
padding-top: 15px;
padding-right: 15px;
z-index: 2000;
background-color: @body-bg;
border: 1px dashed #dbdee0;
.repeater-item-remove {
opacity: 0;
}
.repeater-item-collapsed-title {
top: 5px;
}
}
&.placeholder {
display: block;
position: relative;
height: 25px;
margin-bottom: 5px;
&:before {
display: block;
position: absolute;
.icon(@chevron-right);
color: #d35714;
left: -10px;
top: 8px;
z-index: 2000;
}
}
}
li.field-repeater-item {
position: relative;
margin: 0 0 1em 1em !important;
padding: 1.5em 1.25em 0 1.25em !important;
background: #f5f5f5;
border: 1px solid @input-border;
border-radius: @border-radius-base;
box-shadow: @input-box-shadow;
min-height: 30px;
&:before {
color: #bdc3c7;
.icon(@circle);
font-size: 8px;
position: absolute;
left: -18px;
top: -2px;
}
&:after {
counter-increment: repeater-index-counter;
content: counter(repeater-index-counter);
display: block;
position: absolute;
font-size: 12px;
left: -27px;
color: #bdc3c7;
top: 10px;
width: 24px;
text-align: center;
}
&.collapsed {
.field-repeater-form {
display:none;
}
.repeater-item-collapse {
.repeater-item-collapse-one {
.transform(scale(1,-1));
}
}
.repeater-item-collapsed-title {
display: block;
}
}
.repeater-item-collapsed-title {
display: none;
position: absolute;
font-size: 13px;
top: 50%;
left: 10px;
.transform(translateY(-50%));
}
.field-repeater-form {
position: relative;
top: -7px;
.clearfix;
}
.repeater-item-handle {
position: absolute;
top: -6px;
left: -25px;
color: #bdc3c7;
background: @body-bg;
cursor: move;
opacity: 0;
border-radius: 999px;
.transition(~'opacity 0.5s');
&:hover {
color: #999;
}
}
.repeater-item-collapse {
position: absolute;
top: 5px;
right: 30px;
z-index: 90;
a, button {
.transition(~'transform 0.3s');
color: #bdc3c7;
line-height: 20px;
display: block;
font-size: 12px;
&:hover,
&:focus {
color: #999;
text-decoration: none;
}
}
}
.repeater-item-remove {
position: absolute;
top: 4px;
right: 5px;
z-index: 90;
&.disabled {
display: none;
+ .repeater-item-collapse {
right: 7px;
}
}
.close {
float: none;
display: inline-block;
}
}
.repeater-item-collapse,
.repeater-item-handle,
.repeater-item-remove {
width: 20px;
height: 20px;
text-align: center;
}
&:hover, &:active {
.repeater-item-collapse,
.repeater-item-handle,
.repeater-item-remove {
opacity: 1;
}
}
}
.field-repeater-add-item {
position: relative;
margin-top: 10px;
margin-left: 20px;
border: 2px dotted #e0e0e0;
border-radius: 5px;
&:before {
color: #bdc3c7;
.icon(@plus);
font-size: 16px;
position: absolute;
left: -23px;
top: -11px;
}
> a {
color: #bdc3c7;
text-align: center;
display: block;
text-decoration: none;
padding: 13px 15px;
text-transform: uppercase;
font-weight: 600;
font-size: @font-size-base - 2;
}
&:hover, &:focus {
background-color: @highlight-hover-bg;
border-color: transparent;
&:before {
color: #999;
}
> a {
color: @highlight-hover-text;
}
}
&:active {
background: @highlight-active-bg;
border-color: transparent;
> a {
color: @highlight-active-text;
}
}
&.in-progress {
border-color: #e0e0e0 !important;
background: transparent !important;
}
}
&[data-mode="grid"] {
> ul.field-repeater-items {
display: grid;
gap: 20px;
> .field-repeater-item {
margin-bottom: 0 !important;
}
> .field-repeater-add-item {
margin-top: 0;
> a {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
}
&:before {
display: none;
}
}
}
&[data-columns="2"] ul.field-repeater-items {
grid-template-columns: repeat(2, 1fr);
}
&[data-columns="3"] ul.field-repeater-items {
grid-template-columns: repeat(3, 1fr);
}
&[data-columns="4"] ul.field-repeater-items {
grid-template-columns: repeat(4, 1fr);
@media (max-width: 1600px) {
grid-template-columns: repeat(3, 1fr);
}
}
&[data-columns="5"] ul.field-repeater-items {
grid-template-columns: repeat(5, 1fr);
@media (max-width: 1600px) {
grid-template-columns: repeat(4, 1fr);
}
}
&[data-columns="6"] ul.field-repeater-items {
grid-template-columns: repeat(6, 1fr);
@media (max-width: 1600px) {
grid-template-columns: repeat(4, 1fr);
}
}
@media (min-width: @screen-sm-min) and (max-width: @screen-md-max) {
> ul.field-repeater-items {
grid-template-columns: repeat(2, 1fr) !important;;
}
}
@media (max-width: @screen-xs-max) {
> ul.field-repeater-items {
grid-template-columns: 1fr !important;
> .field-repeater-item,
> .field-repeater-add-item {
min-height: 0 !important;
}
> .field-repeater-add-item {
margin-top: 10px;
&::before {
display: block;
}
}
}
}
}
}
//
// Grouped Repeater Palette
//
.repeater-group-search-container {
padding: 10px 15px;
border-bottom: 1px solid #e0e0e0;
> div {
position: relative;
}
.icon-search {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: #999;
pointer-events: none;
z-index: 10;
}
.repeater-group-search {
padding-left: 32px;
padding-right: 32px;
width: 100%;
}
.repeater-group-search-clear {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: transparent;
border: none;
padding: 4px 6px;
cursor: pointer;
display: none;
z-index: 10;
color: #999;
i {
font-size: 14px;
}
&:hover {
color: #666;
}
}
}
.repeater-group-no-results {
display: none;
padding: 20px;
text-align: center;
color: #999;
}
.repeater-group-items-container {
min-width: 500px;
max-width: 800px;
}
.repeater-group-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 8px;
padding: 15px;
.repeater-group-item {
a {
display: flex;
height: 100%;
align-items: center;
gap: 16px;
padding: 12px 12px 12px 16px;
border: 1px solid #e0e0e0;
border-radius: @border-radius-base;
text-decoration: none;
color: inherit;
transition: all 0.2s;
i {
width: 1em;
text-align: center;
font-size: 24px;
color: #666;
margin-top: 2px;
flex-shrink: 0;
}
> div {
flex: 1;
min-width: 0;
.title {
font-weight: 600;
font-size: 13px;
display: block;
margin-bottom: 2px;
}
.description {
font-size: 11px;
color: #999;
display: block;
line-height: 1.3;
}
}
&:hover {
border-color: @link-color;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
}
}
}

View File

@@ -0,0 +1,87 @@
<div class="field-repeater"
data-control="fieldrepeater"
<?= $titleFrom ? 'data-title-from="'.$titleFrom.'"' : '' ?>
<?= $minItems ? 'data-min-items="'.$minItems.'"' : '' ?>
<?= $maxItems ? 'data-max-items="'.$maxItems.'"' : '' ?>
<?= $style ? 'data-style="'.$style.'"' : '' ?>
data-mode="<?= $mode ?>"
<?php if ($mode === 'grid'): ?>
data-columns="<?= $columns ?>"
<?php endif ?>
<?php if ($sortable): ?>
data-sortable="true"
data-sortable-container="#<?= $this->getId('items') ?>"
data-sortable-handle=".<?= $this->getId('items') ?>-handle"
<?php endif; ?>
>
<?php if (!$this->previewMode): ?>
<input type="hidden" name="<?= $this->getFieldName(); ?>">
<?php endif ?>
<ul id="<?= $this->getId('items') ?>" class="field-repeater-items">
<?php foreach ($formWidgets as $index => $widget): ?>
<?= $this->makePartial('repeater_item', [
'widget' => $widget,
'indexValue' => $index,
]) ?>
<?php endforeach ?>
<?= $this->makePartial('repeater_add_item') ?>
</ul>
<?php if (!$this->previewMode): ?>
<input type="hidden" name="<?= $this->alias; ?>_loaded" value="1">
<?php endif ?>
<script type="text/template" data-group-palette-template>
<div class="popover-head">
<h3><?= e(trans($prompt)) ?></h3>
<button type="button" class="close"
data-dismiss="popover"
aria-hidden="true">&times;</button>
</div>
<div class="repeater-group-search-container">
<div>
<label for="repeater-group-search-<?= $this->getId() ?>" class="sr-only">Search items</label>
<i class="icon-search"></i>
<input type="text"
id="repeater-group-search-<?= $this->getId() ?>"
class="form-control repeater-group-search"
placeholder="Search items..."
autocomplete="off">
<button type="button" class="repeater-group-search-clear">
<i class="icon-close"></i>
</button>
</div>
</div>
<div class="repeater-group-no-results">
No items found
</div>
<div class="popover-fixed-height repeater-group-items-container">
<div class="control-scrollpad" data-control="scrollpad">
<div class="scroll-wrapper">
<div class="control-filelist filelist-hero repeater-group-grid" data-control="filelist">
<?php foreach ($groupDefinitions as $item): ?>
<div class="repeater-group-item">
<a
href="javascript:;"
data-repeater-add
data-request="<?= $this->getEventHandler('onAddItem') ?>"
data-request-data="_repeater_group: '<?= $item['code'] ?>'">
<i class="<?= $item['icon'] ?>"></i>
<div>
<span class="title"><?= e(trans($item['name'])) ?></span>
<span class="description"><?= e(trans($item['description'])) ?></span>
</div>
</a>
</div>
<?php endforeach ?>
</div>
</div>
</div>
</div>
</script>
</div>

View File

@@ -0,0 +1,26 @@
<?php if (!$this->previewMode): ?>
<li
id="<?= $this->getId('add-item') ?>"
class="field-repeater-add-item loading-indicator-container indicator-center"
<?php if ($mode === 'grid'): ?>
style="min-height: <?= $rowHeight ?>px"
<?php endif ?>
>
<?php if ($useGroups): ?>
<a
href="javascript:;"
data-repeater-add-group
data-load-indicator>
<span><?= e(trans($prompt)) ?></span>
</a>
<?php else: ?>
<a
href="javascript:;"
data-repeater-add
data-request="<?= $this->getEventHandler('onAddItem') ?>"
data-load-indicator>
<span><?= e(trans($prompt)) ?></span>
</a>
<?php endif ?>
</li>
<?php endif ?>

View File

@@ -0,0 +1,56 @@
<?php
$groupCode = $useGroups ? $this->getGroupCodeFromIndex($indexValue) : '';
$itemTitle = $useGroups ? $this->getGroupTitle($groupCode) : null;
?>
<li
<?= $itemTitle ? 'data-collapse-title="'.e(trans($itemTitle)).'"' : '' ?>
class="field-repeater-item"
<?php if ($mode === 'grid'): ?>
style="min-height: <?= $rowHeight ?>px"
<?php endif ?>
>
<?php if (!$this->previewMode): ?>
<?php if ($sortable): ?>
<div class="repeater-item-handle <?= $this->getId('items') ?>-handle">
<i class="icon-bars"></i>
</div>
<?php endif; ?>
<div class="repeater-item-remove">
<button
type="button"
class="close"
aria-label="Remove"
data-repeater-remove
data-request="<?= $this->getEventHandler('onRemoveItem') ?>"
data-request-data="'_repeater_index': '<?= $indexValue ?>', '_repeater_group': '<?= $groupCode ?>'"
data-request-confirm="<?= e(trans('backend::lang.form.action_confirm')) ?>">
<span aria-hidden="true">&times;</span>
</button>
</div>
<?php endif ?>
<?php if ($mode !== 'grid'): ?>
<div class="repeater-item-collapse">
<a href="javascript:;" class="repeater-item-collapse-one">
<i class="icon-chevron-up"></i>
</a>
</div>
<div class="repeater-item-collapsed-title"></div>
<?php endif ?>
<div class="field-repeater-form"
data-control="formwidget"
data-refresh-handler="<?= $this->getEventHandler('onRefresh') ?>"
data-refresh-data="'_repeater_index': '<?= $indexValue ?>', '_repeater_group': '<?= $groupCode ?>'">
<?php foreach ($widget->getFields() as $field): ?>
<?= $widget->renderField($field) ?>
<?php endforeach ?>
<?php if ($useGroups): ?>
<input type="hidden" name="<?= $widget->arrayName ?>[_group]" value="<?= $groupCode ?>" />
<?php endif ?>
</div>
</li>