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,50 @@
/*
* Drag-and-drop reordering styles for the list widget.
*/
.control-list .list-sort-handle-column,
.control-list .list-cell-sort-handle {
width: 28px;
text-align: center;
}
/* The whole handle cell is the drag target (SortableJS handle is the cell), so
the grab affordance and hit area cover the entire cell, not just the icon.
The second selector is specific enough to override rowlink's
"tr.rowlink td.nolink { cursor: auto }" rule, which would otherwise reset the
cursor on this (intentionally) non-link cell when the list rows are clickable. */
.control-list .list-cell-sort-handle,
.control-list tr.rowlink td.list-cell-sort-handle.nolink {
cursor: move;
cursor: grab;
}
.control-list .list-cell-sort-handle:active,
.control-list tr.rowlink td.list-cell-sort-handle.nolink:active {
cursor: grabbing;
}
.control-list .list-sort-handle {
display: inline-block;
color: #666;
line-height: 1;
text-decoration: none;
cursor: inherit;
}
.control-list tr:hover .list-sort-handle {
color: #333;
}
.control-list .list-sortable-ghost {
opacity: 0.5;
background: #f0f7fd;
}
.control-list .list-sortable-chosen {
background: #f7f9fa;
}
.control-list[data-sortable="true"] tbody tr {
/* Avoid text selection while dragging rows */
user-select: none;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,99 @@
import Sortable from 'sortablejs';
/*
* List widget drag-and-drop reordering.
*
* Additive enhancement on top of the existing list widget. When a list is rendered with
* `data-sortable="true"`, SortableJS is initialised on its <tbody> using the per-row drag
* handle cell. On drop, the record ids in their new DOM order are posted to the list's reorder
* handler, which assigns the sort order values server-side (by position) and re-renders.
*/
(function ($) {
"use strict";
function collectIds(tbody) {
return Array.prototype.map.call(
tbody.querySelectorAll('tr[data-record-id]'),
function (tr) {
return tr.getAttribute('data-record-id');
}
);
}
function initList(listEl) {
var tbody = listEl.querySelector('tbody');
if (!tbody || tbody.wnListSortable) {
return;
}
var handler = listEl.getAttribute('data-reorder-handler');
if (!handler) {
return;
}
var $list = $(listEl);
// SortableJS dispatches a native "change" event on the list root (the tbody) while
// an item is being dragged. Stop it bubbling to the surrounding form's change monitor
// so reordering — which persists immediately — does not flag the form as having
// unsaved changes. Real form-field changes (target = input/select/textarea) are left
// untouched.
tbody.addEventListener('change', function (event) {
if (event.target === tbody) {
event.stopPropagation();
}
});
var reorderInFlight = false;
tbody.wnListSortable = Sortable.create(tbody, {
handle: '.list-cell-sort-handle',
draggable: 'tr',
filter: '.no-data',
animation: 150,
ghostClass: 'list-sortable-ghost',
chosenClass: 'list-sortable-chosen',
onEnd: function (event) {
// Nothing changed if the row was dropped back in its original position.
if (event.oldIndex === event.newIndex) {
return;
}
// Ignore further drops until the current reorder has been persisted and the
// list re-rendered, so overlapping requests can't race and persist a stale order.
if (reorderInFlight) {
return;
}
reorderInFlight = true;
// The request is fired programmatically (not from a [data-request] element),
// so show the stripe load indicator manually for feedback.
var indicator = ($.wn && $.wn.stripeLoadIndicator) || ($.oc && $.oc.stripeLoadIndicator);
if (indicator) {
indicator.show();
}
// Only the record ids (in their new order) are sent; the server assigns the
// sort order values by position.
$list.request(handler, {
data: { record_ids: collectIds(tbody) }
}).always(function () {
reorderInFlight = false;
if (indicator) {
indicator.hide();
}
});
}
});
}
function initAll() {
var lists = document.querySelectorAll('[data-control="listwidget"][data-sortable="true"]');
Array.prototype.forEach.call(lists, initList);
}
$(document).ready(initAll);
// Re-initialise after the list partial is replaced by an AJAX update (e.g. onRefresh).
$(document).on('render', initAll);
})(window.jQuery);

View File

@@ -0,0 +1,166 @@
/*
* List Widget
*
* Dependences:
* - Row Link Plugin (system/assets/ui/js/list.rowlink.js)
*/
+function ($) { "use strict";
var ListWidget = function (element, options) {
var $el = this.$el = $(element);
this.options = options || {};
var scrollClassContainer = options.scrollClassContainer !== undefined
? options.scrollClassContainer
: $el.parent()
$el.dragScroll({
scrollClassContainer: scrollClassContainer,
scrollSelector: 'thead',
dragSelector: 'thead'
})
this.update()
}
ListWidget.DEFAULTS = {
}
ListWidget.prototype.update = function() {
var
list = this.$el,
head = $('thead', list),
body = $('tbody', list),
foot = $('tfoot', list)
/*
* Bind check boxes
*/
$('.list-checkbox input[type="checkbox"]', body).each(function(){
var $el = $(this)
if ($el.is(':checked'))
$el.closest('tr').addClass('active')
})
head.on('change', '.list-checkbox input[type="checkbox"]', function(){
var $el = $(this),
checked = $el.is(':checked')
$('.list-checkbox input[type="checkbox"]', body).prop('checked', checked)
if (checked)
$('tr', body).addClass('active')
else
$('tr', body).removeClass('active')
})
body.on('change', '.list-checkbox input[type="checkbox"]', function(){
var $el = $(this),
checked = $el.is(':checked')
if (checked) {
$el.closest('tr').addClass('active')
}
else {
$('.list-checkbox input[type="checkbox"]', head).prop('checked', false)
$el.closest('tr').removeClass('active')
}
})
this.lastChecked = null
body.on('click', '.list-checkbox input[type="checkbox"]', (e) => {
const current = e.currentTarget
if (this.lastChecked && e.shiftKey) {
const checkboxes = $('.list-checkbox input[type="checkbox"]', body)
const start = checkboxes.index(current)
const end = checkboxes.index(this.lastChecked)
checkboxes
.slice(Math.min(start, end), Math.max(start, end) + 1)
.each(function () {
$(this).prop('checked', current.checked).trigger('change')
})
}
this.lastChecked = current
})
}
ListWidget.prototype.getChecked = function() {
var
list = this.$el,
body = $('tbody', list)
return $('.list-checkbox input[type="checkbox"]', body).map(function(){
var $el = $(this)
if ($el.is(':checked'))
return $el.val()
}).get();
}
ListWidget.prototype.toggleChecked = function(el) {
var $checkbox = $('.list-checkbox input[type="checkbox"]', $(el).closest('tr'))
$checkbox.prop('checked', !$checkbox.is(':checked')).trigger('change')
}
// LIST WIDGET PLUGIN DEFINITION
// ============================
var old = $.fn.listWidget
$.fn.listWidget = function (option) {
var args = Array.prototype.slice.call(arguments, 1), result
this.each(function () {
var $this = $(this)
var data = $this.data('oc.listwidget')
var options = $.extend({}, ListWidget.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.listwidget', (data = new ListWidget(this, options)))
if (typeof option == 'string') result = data[option].apply(data, args)
if (typeof result != 'undefined') return false
})
return result ? result : this
}
$.fn.listWidget.Constructor = ListWidget
// LIST WIDGET NO CONFLICT
// =================
$.fn.listWidget.noConflict = function () {
$.fn.listWidget = old
return this
}
// LIST WIDGET HELPERS
// =================
if ($.wn === undefined)
$.wn = {}
if ($.oc === undefined)
$.oc = $.wn
$.wn.listToggleChecked = function(el) {
$(el)
.closest('[data-control="listwidget"]')
.listWidget('toggleChecked', el)
}
$.wn.listGetChecked = function(el) {
return $(el)
.closest('[data-control="listwidget"]')
.listWidget('getChecked')
}
// LIST WIDGET DATA-API
// ==============
$(document).render(function(){
$('[data-control="listwidget"]').listWidget();
})
}(window.jQuery);