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,130 @@
import { delegate } from 'jquery-events-to-dom-events';
/**
* Backend AJAX handler.
*
* This is a utility script that resolves some backwards-compatibility issues with the functionality
* that relies on the old framework, and ensures that Snowboard works well within the Backend
* environment.
*
* Functions:
* - Adds the "render" jQuery event to Snowboard requests that widgets use to initialise.
* - Ensures the CSRF token is included in requests.
*
* @copyright 2021 Winter.
* @author Ben Thomson <git@alfreido.com>
*/
export default class Handler extends Snowboard.Singleton {
/**
* Event listeners.
*
* @returns {Object}
*/
listens() {
return {
ready: 'ready',
ajaxFetchOptions: 'ajaxFetchOptions',
ajaxUpdateComplete: 'ajaxUpdateComplete',
};
}
/**
* Ready handler.
*
* Fires off a "render" event.
*/
ready() {
if (!window.jQuery) {
return;
}
delegate('render');
// Add global event for rendering in Snowboard
delegate('render');
document.addEventListener('$render', () => {
this.snowboard.globalEvent('render');
});
// Add "render" event for backwards compatibility
window.jQuery(document).trigger('render');
// Add global event for rendering in Snowboard
document.addEventListener('$render', () => {
this.snowboard.globalEvent('render');
});
}
/**
* Adds the jQuery AJAX prefilter that the old framework uses to inject the CSRF token in AJAX
* calls.
*/
addPrefilter() {
if (!window.jQuery) {
return;
}
window.jQuery.ajaxPrefilter((options) => {
if (this.hasToken()) {
if (!options.headers) {
options.headers = {};
}
options.headers['X-CSRF-TOKEN'] = this.getToken();
}
});
}
/**
* Fetch options handler.
*
* Ensures that the CSRF token is included in Snowboard requests.
*
* @param {Object} options
*/
ajaxFetchOptions(options) {
if (this.hasToken()) {
options.headers['X-CSRF-TOKEN'] = this.getToken();
}
}
/**
* Update complete handler.
*
* Fires off a "render" event when partials are updated so that any widgets included in
* responses are correctly initialised.
*/
ajaxUpdateComplete() {
if (!window.jQuery) {
return;
}
// Add "render" event for backwards compatibility
window.jQuery(document).trigger('render');
}
/**
* Determines if a CSRF token is available.
*
* @returns {Boolean}
*/
hasToken() {
const tokenElement = document.querySelector('meta[name="csrf-token"]');
if (!tokenElement) {
return false;
}
if (!tokenElement.hasAttribute('content')) {
return false;
}
return true;
}
/**
* Gets the CSRF token.
*
* @returns {String}
*/
getToken() {
return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
}
}

View File

@@ -0,0 +1 @@
"use strict";(self.webpackChunk_wintercms_wn_backend_module=self.webpackChunk_wintercms_wn_backend_module||[]).push([[476],{286:function(e,t,n){var i=n(35),r=n(171);class s extends Snowboard.Singleton{listens(){return{ready:"ready",ajaxFetchOptions:"ajaxFetchOptions",ajaxUpdateComplete:"ajaxUpdateComplete"}}ready(){window.jQuery&&((0,r.M)("render"),(0,r.M)("render"),document.addEventListener("$render",()=>{this.snowboard.globalEvent("render")}),window.jQuery(document).trigger("render"),document.addEventListener("$render",()=>{this.snowboard.globalEvent("render")}))}addPrefilter(){window.jQuery&&window.jQuery.ajaxPrefilter(e=>{this.hasToken()&&(e.headers||(e.headers={}),e.headers["X-CSRF-TOKEN"]=this.getToken())})}ajaxFetchOptions(e){this.hasToken()&&(e.headers["X-CSRF-TOKEN"]=this.getToken())}ajaxUpdateComplete(){window.jQuery&&window.jQuery(document).trigger("render")}hasToken(){const e=document.querySelector('meta[name="csrf-token"]');return!!e&&!!e.hasAttribute("content")}getToken(){return document.querySelector('meta[name="csrf-token"]').getAttribute("content")}}class a extends Snowboard.PluginBase{construct(e,t){if(e instanceof Snowboard.PluginBase==!1)throw new Error("Event handling can only be applied to Snowboard classes.");if(!t)throw new Error("Event prefix is required.");this.instance=e,this.eventPrefix=t,this.events=[]}on(e,t){this.events.push({event:e,callback:t})}off(e,t){this.events=this.events.filter(n=>n.event!==e||n.callback!==t)}once(e,t){const n=this.events.push({event:e,callback:(...e)=>{t(...e),this.events.splice(n-1,1)}})}fire(e,...t){const n=this.events.filter(t=>t.event===e);let i=!1;n.forEach(e=>{i||!1===e.callback(...t)&&(i=!0)}),i||this.snowboard.globalEvent(`${this.eventPrefix}.${e}`,...t)}firePromise(e,...t){const n=this.events.filter(t=>t.event===e),i=n.filter(e=>null!==e,n.map(e=>e.callback(...t)));Promise.all(i).then(()=>{this.snowboard.globalPromiseEvent(`${this.eventPrefix}.${e}`,...t)})}}class o extends Snowboard.Singleton{construct(){this.registeredWidgets=[],this.elements=[],this.events={mutate:e=>this.onMutation(e)},this.observer=null}listens(){return{ready:"onReady",render:"onRender",ajaxUpdate:"onAjaxUpdate"}}register(e,t,n){this.registeredWidgets.push({control:e,widget:t,callback:n})}unregister(e){this.registeredWidgets=this.registeredWidgets.filter(t=>t.control!==e)}onReady(){this.initializeWidgets(document.body),this.observer||(this.observer=new MutationObserver(this.events.mutate),this.observer.observe(document.body,{childList:!0,subtree:!0}))}onRender(){this.initializeWidgets(document.body)}onAjaxUpdate(e){this.initializeWidgets(e)}initializeWidgets(e){this.registeredWidgets.forEach(t=>{const n=e.querySelectorAll(`[data-control="${t.control}"]:not([data-widget-initialized])`);n.length&&n.forEach(e=>{if(e.dataset.widgetInitialized)return;const n=this.snowboard[t.widget](e);this.elements.push({element:e,instance:n}),e.dataset.widgetInitialized=!0,this.snowboard.globalEvent("backend.widget.initialized",e,n),"function"==typeof t.callback&&t.callback(n,e)})})}getWidget(e){const t=this.elements.find(t=>t.element===e);return t?t.instance:null}onMutation(e){const t=e.filter(e=>e.removedNodes.length).map(e=>Array.from(e.removedNodes)).flat();t.length&&t.forEach(e=>{const t=this.elements.filter(t=>e.contains(t.element));t.length&&t.forEach(e=>{e.instance.destruct(),this.elements=this.elements.filter(t=>t!==e)})})}}if(void 0===window.Snowboard)throw new Error("Snowboard must be loaded in order to use the Backend UI.");(e=>{e.addPlugin("backend.ajax.handler",s),e.addPlugin("backend.ui.eventHandler",a),e.addPlugin("backend.ui.widgetHandler",o),e["backend.ajax.handler"]().addPrefilter(),window.AssetManager={load:(t,n)=>{e.assetLoader().load(t).then(()=>{n&&"function"==typeof n&&n()})}},window.assetManager=window.AssetManager})(window.Snowboard),window.Vue=i}},function(e){e.O(0,[810],function(){return t=286,e(e.s=t);var t});e.O()}]);

View File

@@ -0,0 +1 @@
!function(){"use strict";var n,e={},r={};function t(n){var o=r[n];if(void 0!==o)return o.exports;var i=r[n]={id:n,exports:{}};return e[n](i,i.exports,t),i.exports}t.m=e,n=[],t.O=function(e,r,o,i){if(!r){var u=1/0;for(l=0;l<n.length;l++){r=n[l][0],o=n[l][1],i=n[l][2];for(var f=!0,c=0;c<r.length;c++)(!1&i||u>=i)&&Object.keys(t.O).every(function(n){return t.O[n](r[c])})?r.splice(c--,1):(f=!1,i<u&&(u=i));if(f){n.splice(l--,1);var a=o();void 0!==a&&(e=a)}}return e}i=i||0;for(var l=n.length;l>0&&n[l-1][2]>i;l--)n[l]=n[l-1];n[l]=[r,o,i]},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,{a:e}),e},t.d=function(n,e){for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},function(){var n={624:0};t.O.j=function(e){return 0===n[e]};var e=function(e,r){var o,i,u=r[0],f=r[1],c=r[2],a=0;if(u.some(function(e){return 0!==n[e]})){for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(c)var l=c(t)}for(e&&e(r);a<u.length;a++)i=u[a],t.o(n,i)&&n[i]&&n[i][0](),n[i]=0;return t.O(l)},r=self.webpackChunk_wintercms_wn_backend_module=self.webpackChunk_wintercms_wn_backend_module||[];r.forEach(e.bind(null,0)),r.push=e.bind(null,r.push.bind(r))}(),t.nc=void 0}();

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,34 @@
import * as Vue from 'vue';
import BackendAjaxHandler from './ajax/Handler';
import BackendUiEventHandler from './ui/EventHandler';
import BackendUiWidgetHandler from './ui/WidgetHandler';
if (window.Snowboard === undefined) {
throw new Error('Snowboard must be loaded in order to use the Backend UI.');
}
((Snowboard) => {
Snowboard.addPlugin('backend.ajax.handler', BackendAjaxHandler);
Snowboard.addPlugin('backend.ui.eventHandler', BackendUiEventHandler);
Snowboard.addPlugin('backend.ui.widgetHandler', BackendUiWidgetHandler);
// Add the pre-filter immediately
Snowboard['backend.ajax.handler']().addPrefilter();
// Add polyfill for AssetManager
window.AssetManager = {
load: (assets, callback) => {
Snowboard.assetLoader().load(assets).then(
() => {
if (callback && typeof callback === 'function') {
callback();
}
},
);
},
};
window.assetManager = window.AssetManager;
})(window.Snowboard);
// Add Vue to global scope
window.Vue = Vue;

View File

@@ -0,0 +1,100 @@
import { delegate } from 'jquery-events-to-dom-events';
((Snowboard) => {
class Preferences extends Snowboard.Singleton {
construct() {
this.widget = null;
}
listens() {
return {
'backend.widget.initialized': 'onWidgetInitialized',
};
}
onWidgetInitialized(element, widget) {
if (element === document.getElementById('CodeEditor-formEditorPreview-_editor_preview')) {
this.widget = widget;
this.enablePreferences();
}
}
enablePreferences() {
delegate('change');
const checkboxes = {
show_gutter: 'showGutter',
highlight_active_line: 'highlightActiveLine',
use_hard_tabs: '!useSoftTabs',
display_indent_guides: 'displayIndentGuides',
show_invisibles: 'showInvisibles',
show_print_margin: 'showPrintMargin',
show_minimap: 'showMinimap',
enable_folding: 'codeFolding',
bracket_colors: 'bracketColors',
show_colors: 'showColors',
};
Object.entries(checkboxes).forEach(([key, value]) => {
this.element(key).addEventListener('change', (event) => {
this.widget.setConfig(
value.replace(/^!/, ''),
/^!/.test(value) ? !event.target.checked : event.target.checked,
);
});
});
this.element('theme').addEventListener('$change', (event) => {
this.widget.loadTheme(event.target.value);
});
this.element('font_size').addEventListener('$change', (event) => {
this.widget.setConfig('fontSize', event.target.value);
});
this.element('tab_size').addEventListener('$change', (event) => {
this.widget.setConfig('tabSize', event.target.value);
});
this.element('word_wrap').addEventListener('$change', (event) => {
const { value } = event.target;
switch (value) {
case 'off':
this.widget.setConfig('wordWrap', false);
break;
case 'fluid':
this.widget.setConfig('wordWrap', 'fluid');
break;
default:
this.widget.setConfig('wordWrap', parseInt(value, 10));
}
});
document.querySelectorAll('[data-switch-lang]').forEach((element) => {
element.addEventListener('click', (event) => {
event.preventDefault();
const language = element.dataset.switchLang;
const template = document.querySelector(`[data-lang-snippet="${language}"]`);
if (!template) {
return;
}
this.widget.setValue(template.textContent.trim());
this.widget.setLanguage(language);
});
});
this.widget.events.once('create', () => {
const event = new MouseEvent('click');
document.querySelector('[data-switch-lang="css"]').dispatchEvent(event);
});
}
element(key) {
return document.getElementById(`Form-field-Preference-editor_${key}`);
}
}
Snowboard.addPlugin('backend.preferences', Preferences);
})(window.Snowboard);

View File

@@ -0,0 +1,116 @@
/**
* Widget event handler.
*
* Extends a widget with event handling functionality, allowing for the quick definition of events
* and listening for events on a specific instance of a widget.
*
* This is a complement to Snowboard's global events - these events will still fire in order to
* allow external code to listen and handle events. Local events can cancel the global event (and
* further local events) by returning `false` from the callback.
*
* @copyright 2022 Winter.
* @author Ben Thomson <git@alfreido.com>
*/
export default class EventHandler extends Snowboard.PluginBase {
/**
* Constructor.
*
* @param {PluginBase} instance
* @param {String} eventPrefix
*/
construct(instance, eventPrefix) {
if (instance instanceof Snowboard.PluginBase === false) {
throw new Error('Event handling can only be applied to Snowboard classes.');
}
if (!eventPrefix) {
throw new Error('Event prefix is required.');
}
this.instance = instance;
this.eventPrefix = eventPrefix;
this.events = [];
}
/**
* Registers a listener for a widget's event.
*
* @param {String} event
* @param {Function} callback
*/
on(event, callback) {
this.events.push({
event,
callback,
});
}
/**
* Deregisters a listener for a widget's event.
*
* @param {String} event
* @param {Function} callback
*/
off(event, callback) {
this.events = this.events.filter((registeredEvent) => registeredEvent.event !== event || registeredEvent.callback !== callback);
}
/**
* Registers a listener for a widget's event that will only fire once.
*
* @param {String} event
* @param {Function} callback
*/
once(event, callback) {
const length = this.events.push({
event,
callback: (...parameters) => {
callback(...parameters);
this.events.splice(length - 1, 1);
},
});
}
/**
* Fires an event on the widget.
*
* Local events are fired first, then a global event is fired afterwards.
*
* @param {String} eventName
* @param {...any} parameters
*/
fire(eventName, ...parameters) {
// Fire local events first
const events = this.events.filter((registeredEvent) => registeredEvent.event === eventName);
let cancelled = false;
events.forEach((event) => {
if (cancelled) {
return;
}
if (event.callback(...parameters) === false) {
cancelled = true;
}
});
if (!cancelled) {
this.snowboard.globalEvent(`${this.eventPrefix}.${eventName}`, ...parameters);
}
}
/**
* Fires a promise event on the widget.
*
* Local events are fired first, then a global event is fired afterwards.
*
* @param {String} eventName
* @param {...any} parameters
*/
firePromise(eventName, ...parameters) {
const events = this.events.filter((registeredEvent) => registeredEvent.event === eventName);
const promises = events.filter((event) => event !== null, events.map((event) => event.callback(...parameters)));
Promise.all(promises).then(
() => {
this.snowboard.globalPromiseEvent(`${this.eventPrefix}.${eventName}`, ...parameters);
},
);
}
}

View File

@@ -0,0 +1,181 @@
/**
* Backend widget handler.
*
* Handles the creation and disposal of widgets in the Backend. Widgets should include this as
* a dependency in order to be loaded and initialised after the handler, in order to correctly
* register.
*
* @copyright 2022 Winter.
* @author Ben Thomson <git@alfreido.com>
*/
export default class WidgetHandler extends Snowboard.Singleton {
/**
* Constructor.
*/
construct() {
this.registeredWidgets = [];
this.elements = [];
this.events = {
mutate: (mutations) => this.onMutation(mutations),
};
this.observer = null;
}
/**
* Listeners.
*
* @returns {Object}
*/
listens() {
return {
ready: 'onReady',
render: 'onRender',
ajaxUpdate: 'onAjaxUpdate',
};
}
/**
* Registers a widget as a given data control.
*
* Registering a widget will allow any element that contains a "data-control" attribute matching
* the control name to be initialized with the given widget.
*
* You may optionally provide a callback that will be fired when an instance of the widget is
* initialized - the callback will be provided the element and the widget instance as parameters.
*
* @param {String} control
* @param {Snowboard.PluginBase} widget
* @param {Function} callback
*/
register(control, widget, callback) {
this.registeredWidgets.push({
control,
widget,
callback,
});
}
/**
* Unregisters a data control.
*
* @param {String} control
*/
unregister(control) {
this.registeredWidgets = this.registeredWidgets.filter((widget) => widget.control !== control);
}
/**
* Ready handler.
*
* Initializes widgets within the entire document.
*/
onReady() {
this.initializeWidgets(document.body);
// Register a DOM observer and watch for any removed nodes
if (!this.observer) {
this.observer = new MutationObserver(this.events.mutate);
this.observer.observe(document.body, {
childList: true,
subtree: true,
});
}
}
/**
* Render handler.
*
* Initializes widgets within the entire document.
*/
onRender() {
this.initializeWidgets(document.body);
}
/**
* AJAX update handler.
*
* Initializes widgets inside an update element from an AJAX response.
*
* @param {HTMLElement} element
*/
onAjaxUpdate(element) {
this.initializeWidgets(element);
}
/**
* Initializes all widgets within an element.
*
* If an element contains a "data-control" attribute matching a registered widget, the widget
* is initialized and attached to the element as a "widget" property.
*
* Only one widget may be initialized to a particular element.
*
* @param {HTMLElement} element
*/
initializeWidgets(element) {
this.registeredWidgets.forEach((widget) => {
const instances = element.querySelectorAll(`[data-control="${widget.control}"]:not([data-widget-initialized])`);
if (instances.length) {
instances.forEach((instance) => {
// Prevent double-widget initialization
if (instance.dataset.widgetInitialized) {
return;
}
const widgetInstance = this.snowboard[widget.widget](instance);
this.elements.push({
element: instance,
instance: widgetInstance,
});
instance.dataset.widgetInitialized = true;
this.snowboard.globalEvent('backend.widget.initialized', instance, widgetInstance);
if (typeof widget.callback === 'function') {
widget.callback(widgetInstance, instance);
}
});
}
});
}
/**
* Returns a widget that is attached to the given element, if any.
*
* @param {HTMLElement} element
* @returns {Snowboard.PluginBase|null}
*/
getWidget(element) {
const found = this.elements.find((widget) => widget.element === element);
if (found) {
return found.instance;
}
return null;
}
/**
* Callback for mutation events.
*
* We're only tracking removed nodes, to ensure that those widgets are disposed of.
*
* @param {MutationRecord[]} mutations
*/
onMutation(mutations) {
const removedNodes = mutations.filter((mutation) => mutation.removedNodes.length).map((mutation) => Array.from(mutation.removedNodes)).flat();
if (!removedNodes.length) {
return;
}
removedNodes.forEach((node) => {
const widgets = this.elements.filter((widget) => node.contains(widget.element));
if (widgets.length) {
widgets.forEach((widget) => {
widget.instance.destruct();
this.elements = this.elements.filter((element) => element !== widget);
});
}
});
}
}