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,219 @@
.field-mediafinder {
overflow: hidden;
}
.field-mediafinder .find-object {
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
position: relative;
outline: none;
overflow: hidden;
display: inline-block;
vertical-align: top;
}
.field-mediafinder .find-object .icon-container {
display: table;
}
.field-mediafinder .find-object .icon-container i {
color: #95a5a6;
display: inline-block;
}
.field-mediafinder .find-object h4 {
font-weight: 600;
font-size: 13px;
color: #2b3e50;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 150%;
margin: 15px 0 5px 0;
padding-right: 0;
-webkit-transition: padding 0.1s;
transition: padding 0.1s;
position: relative;
}
.field-mediafinder .find-object h4 a {
position: absolute;
right: 0;
top: 0;
display: none;
font-weight: 400;
}
.field-mediafinder .find-object .info h4 a,
.field-mediafinder .find-object .meta a.find-remove-button {
color: #2b3e50;
display: none;
font-size: 15px;
text-decoration: none;
}
.field-mediafinder.is-populated .find-object:hover h4 a,
.field-mediafinder.is-populated .find-object:hover .meta .find-remove-button {
display: block;
}
.field-mediafinder.is-preview .find-button,
.field-mediafinder.is-preview .find-remove-button {
display: none !important;
}
@media (max-width: 1024px) {
.field-fileupload.is-populated .find-object h4 a,
.field-fileupload.is-populated .find-object .meta .find-remove-button {
display: block !important;
}
}
.field-mediafinder.style-image-single .find-button {
display: block;
float: left;
border: 2px dotted rgba(0, 0, 0, 0.1);
position: relative;
outline: none;
min-height: 100px;
min-width: 100px;
}
.field-mediafinder.style-image-single .find-button .find-button-icon {
position: absolute;
width: 22px;
height: 22px;
top: 50%;
left: 50%;
margin-top: -11px;
margin-left: -11px;
}
.field-mediafinder.style-image-single .find-button .find-button-icon:before {
text-align: center;
display: block;
font-size: 22px;
height: 22px;
width: 22px;
line-height: 22px;
color: rgba(0, 0, 0, 0.1);
}
.field-mediafinder.style-image-single .find-button:hover {
border: 2px dotted rgba(0, 0, 0, 0.2);
}
.field-mediafinder.style-image-single .find-button:hover .find-button-icon:before {
color: #31ac5f;
color: rgba(0, 0, 0, 0.2);
}
.field-mediafinder.style-image-single .find-button:focus {
border: 2px solid rgba(0, 0, 0, 0.3);
background: transparent;
}
.field-mediafinder.style-image-single .find-button:focus .find-button-icon:before {
color: #31ac5f;
color: rgba(0, 0, 0, 0.2);
}
.field-mediafinder.style-image-single .find-object {
display: none;
padding-bottom: 39px;
}
.field-mediafinder.style-image-single .find-object .icon-container {
display: block;
border: 1px solid #f6f8f9;
background: rgba(255, 255, 255, 0.5);
}
.field-mediafinder.style-image-single .find-object .icon-container img {
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
display: block;
max-width: 100%;
height: auto;
}
.field-mediafinder.style-image-single .find-object .info {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 39px;
}
.field-mediafinder.style-image-single .find-object:hover h4 {
padding-right: 20px;
}
.field-mediafinder.style-image-single.is-populated .find-button {
display: none;
}
.field-mediafinder.style-image-single.is-populated .find-object {
display: block;
}
.field-mediafinder.style-file-single {
background-color: #ffffff;
border: 1px solid #bdc3c7;
overflow: hidden;
position: relative;
padding-right: 30px;
border-radius: 3px;
-webkit-box-shadow: inset 0 1px 0 #bdc3c7, 0 1px 0 #fff;
box-shadow: inset 0 1px 0 #bdc3c7, 0 1px 0 #fff;
}
.field-mediafinder.style-file-single .find-button {
position: absolute;
top: 50%;
margin-top: -44px;
height: 88px;
background: transparent;
right: -2px;
color: #595959;
}
.field-mediafinder.style-file-single .find-button i {
font-size: 14px;
}
.field-mediafinder.style-file-single .find-button:hover {
color: #333333;
}
.field-mediafinder.style-file-single .find-empty-message {
padding: 8px 0 8px 11px;
font-size: 14px;
}
.field-mediafinder.style-file-single .find-object {
display: none;
width: 100%;
padding: 7px 0 9px 0;
}
.field-mediafinder.style-file-single .find-object .icon-container {
position: absolute;
top: 0;
left: 0;
width: 15px;
padding: 0 5px;
margin: 8px 0 0 7px;
text-align: center;
}
.field-mediafinder.style-file-single .find-object .icon-container i {
line-height: 150%;
font-size: 15px;
}
.field-mediafinder.style-file-single .find-object .info {
margin-left: 34px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.field-mediafinder.style-file-single .find-object .info h4 {
display: inline;
margin: 0;
padding: 0;
font-size: 13px;
line-height: 150%;
color: #666666;
}
.field-mediafinder.style-file-single .find-object .meta {
position: absolute;
top: 50%;
margin-top: -44px;
height: 88px;
right: 0;
}
.field-mediafinder.style-file-single .find-object .meta .find-remove-button {
position: absolute;
top: 50%;
right: 0;
height: 20px;
margin-top: -11px;
margin-right: 10px;
}
.field-mediafinder.style-file-single.is-populated .find-empty-message {
display: none;
}
.field-mediafinder.style-file-single.is-populated .find-object {
display: block;
}

View File

@@ -0,0 +1,177 @@
/*
* MediaFinder plugin
*
* Data attributes:
* - data-control="mediafinder" - enables the plugin on an element
* - data-option="value" - an option with a value
*
* JavaScript API:
* $('a#someElement').mediaFinder({ option: 'value' })
*
* Dependences:
* - Some other plugin (filename.js)
*/
+function ($) { "use strict";
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var MediaFinder = function (element, options) {
this.$el = $(element)
this.options = options || {}
$.wn.foundation.controlUtils.markDisposable(element)
Base.call(this)
this.init()
}
MediaFinder.prototype = Object.create(BaseProto)
MediaFinder.prototype.constructor = MediaFinder
MediaFinder.prototype.init = function() {
if (this.options.isMulti === null) {
this.options.isMulti = this.$el.hasClass('is-multi')
}
if (this.options.isPreview === null) {
this.options.isPreview = this.$el.hasClass('is-preview')
}
if (this.options.isImage === null) {
this.options.isImage = this.$el.hasClass('is-image')
}
if (this.options.mode === null) {
this.options.mode = this.$el.data('mediafinder-mode') || 'all';
}
this.$el.one('dispose-control', this.proxy(this.dispose))
if (this.options.thumbnailWidth > 0) {
this.$el.find('[data-find-image]').css('maxWidth', this.options.thumbnailWidth + 'px')
}
else if (this.options.thumbnailHeight > 0) {
this.$el.find('[data-find-image]').css('maxHeight', this.options.thumbnailHeight + 'px')
}
// Stop here for preview mode
if (this.options.isPreview) {
return
}
this.$el.on('click', '.find-button', this.proxy(this.onClickFindButton))
this.$el.on('click', '.find-empty-message', this.proxy(this.onClickFindButton)).css({'cursor':'pointer'})
this.$el.on('click', '.find-remove-button', this.proxy(this.onClickRemoveButton))
this.$findValue = $('[data-find-value]', this.$el)
}
MediaFinder.prototype.dispose = function() {
this.$el.off('click', '.find-button', this.proxy(this.onClickFindButton))
this.$el.off('click', '.find-remove-button', this.proxy(this.onClickRemoveButton))
this.$el.off('dispose-control', this.proxy(this.dispose))
this.$el.removeData('oc.mediaFinder')
this.$findValue = null
this.$el = null
// In some cases options could contain callbacks,
// so it's better to clean them up too.
this.options = null
BaseProto.dispose.call(this)
}
MediaFinder.prototype.setValue = function(value) {
// set value and trigger change event, so that wrapping implementations
// like mlmediafinder can listen for changes.
this.$findValue.val(value).trigger('change')
}
MediaFinder.prototype.onClickRemoveButton = function() {
this.setValue('')
this.evalIsPopulated()
}
MediaFinder.prototype.onClickFindButton = function() {
var self = this
new $.wn.mediaManager.popup({
alias: 'ocmediamanager',
cropAndInsertButton: ['image', 'all'].includes(self.options.mode),
mode: self.options.mode,
onInsert: function(items) {
if (!items.length) {
alert('Please select image(s) to insert.')
return
}
if (items.length > 1) {
alert('Please select a single item.')
return
}
var path, publicUrl
for (var i=0, len=items.length; i<len; i++) {
path = items[i].path
publicUrl = items[i].publicUrl
}
self.setValue(path)
if (self.options.isImage) {
$('[data-find-image]', self.$el).attr('src', publicUrl)
$('[data-find-error]', self.$el).hide()
}
self.evalIsPopulated()
this.hide()
}
})
}
MediaFinder.prototype.evalIsPopulated = function() {
var isPopulated = !!this.$findValue.val()
this.$el.toggleClass('is-populated', isPopulated)
$('[data-find-file-name]', this.$el).text(this.$findValue.val().substring(1))
}
MediaFinder.DEFAULTS = {
isMulti: null,
isPreview: null,
isImage: null,
mode: null
}
// PLUGIN DEFINITION
// ============================
var old = $.fn.mediaFinder
$.fn.mediaFinder = function(option) {
var args = arguments;
return this.each(function() {
var $this = $(this)
var data = $this.data('oc.mediaFinder')
var options = $.extend({}, MediaFinder.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.mediaFinder', (data = new MediaFinder(this, options)))
if (typeof option == 'string') data[option].apply(data, args)
})
}
$.fn.mediaFinder.Constructor = MediaFinder
$.fn.mediaFinder.noConflict = function() {
$.fn.mediaFinder = old
return this
}
$(document).render(function() {
$('[data-control="mediafinder"]').mediaFinder()
})
}(window.jQuery);

View File

@@ -0,0 +1,143 @@
.finder-block-button() {
display: block;
float: left;
border: 2px dotted rgba(0,0,0,.1);
position: relative;
outline: none;
.find-button-icon {
position: absolute;
width: 22px;
height: 22px;
top: 50%;
left: 50%;
margin-top: -11px;
margin-left: -11px;
&:before {
text-align: center;
display: block;
font-size: 22px;
height: 22px;
width: 22px;
line-height: 22px;
color: rgba(0,0,0,.1);
}
}
&:hover {
border: 2px dotted rgba(0,0,0,.2);
.find-button-icon:before {
color: @brand-success;
color: rgba(0,0,0,.2);
}
}
&:focus {
border: 2px solid rgba(0,0,0,.3);
background: transparent;
.find-button-icon:before {
color: @brand-success;
color: rgba(0,0,0,.2);
}
}
}
.finder-vertical-align() {
position: absolute;
top: 50%;
margin-top: -44px;
height: 88px;
}
//
// Shared
//
.field-mediafinder {
overflow: hidden;
.find-object {
.border-radius(3px);
position: relative;
outline: none;
overflow: hidden;
display: inline-block;
vertical-align: top;
.icon-container {
display: table;
i {
color: #95a5a6;
display: inline-block;
}
}
h4 {
font-weight: 600;
font-size: 13px;
color: #2b3e50;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 150%;
margin: 15px 0 5px 0;
padding-right: 0;
.transition(padding 0.1s);
position: relative;
a {
position: absolute;
right: 0;
top: 0;
display: none;
font-weight: 400;
}
}
.info h4 a,
.meta a.find-remove-button {
color: #2b3e50;
display: none;
font-size: 15px;
text-decoration: none;
}
}
&.is-populated {
.find-object:hover {
h4 a,
.meta .find-remove-button { display: block; }
}
}
//
// Preview mode
//
&.is-preview {
.find-button,
.find-remove-button {
display: none !important;
}
}
}
//
// Media
//
@media (max-width: 1024px) {
.field-fileupload {
&.is-populated .find-object {
h4 a,
.meta .find-remove-button { display: block !important; }
}
}
}

View File

@@ -0,0 +1,90 @@
.field-mediafinder.style-file-single {
background-color: @color-form-field-bg;
border: 1px solid @color-form-field-border;
overflow: hidden;
position: relative;
padding-right: 30px;
border-radius: 3px;
.box-shadow(@input-box-shadow);
.find-button {
.finder-vertical-align();
background: transparent;
right: -2px;
color: lighten(@color-form-field-recordfinder-btn, 15%);
i {
font-size: 14px;
}
&:hover {
color: @color-form-field-recordfinder-btn;
}
}
.find-empty-message {
padding: 8px 0 8px 11px;
font-size: 14px;
}
.find-object {
display: none;
width: 100%;
padding: 7px 0 9px 0;
.icon-container {
position: absolute;
top: 0;
left: 0;
width: 15px;
padding: 0 5px;
margin: 8px 0 0 7px;
text-align: center;
i {
line-height: 150%;
font-size: 15px;
}
}
.info {
margin-left: 34px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
h4 {
display: inline;
margin: 0;
padding: 0;
font-size: 13px;
line-height: 150%;
color: #666666;
}
}
.meta {
.finder-vertical-align();
right: 0;
.find-remove-button {
position: absolute;
top: 50%;
right: 0;
height: 20px;
margin-top: -11px;
margin-right: 10px;
}
}
}
&.is-populated {
.find-empty-message {
display: none;
}
.find-object {
display: block;
}
}
}

View File

@@ -0,0 +1,47 @@
.field-mediafinder.style-image-single {
.find-button {
.finder-block-button();
min-height: 100px;
min-width: 100px;
}
.find-object {
display: none;
padding-bottom: 39px;
.icon-container {
display: block;
border: 1px solid #f6f8f9;
background: rgba(255,255,255,.5);
img {
.border-radius(3px);
.img-responsive();
}
}
.info {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 39px;
}
&:hover {
h4 { padding-right: 20px; }
}
}
&.is-populated {
.find-button {
display: none;
}
.find-object {
display: block;
}
}
}

View File

@@ -0,0 +1,5 @@
@import "../../../../../backend/assets/less/core/boot.less";
@import "mediafinder.base.less";
@import "mediafinder.imagesingle.less";
@import "mediafinder.filesingle.less";

View File

@@ -0,0 +1,53 @@
<div
id="<?= $this->getId() ?>"
class="
field-mediafinder
style-file-single
<?php if ($value): ?>
is-populated
<?php endif ?>
<?php if ($this->previewMode): ?>
is-preview
<?php endif ?>
"
data-control="mediafinder"
data-mediafinder-mode="<?= $mode ?? 'all' ?>"
<?= $field->getAttributes() ?>
>
<!-- Find Button -->
<button type="button" class="btn btn-default find-button">
<i class="icon-folder"></i>
</button>
<!-- Existing value -->
<div class="find-object">
<div class="icon-container">
<i class="icon-file"></i>
</div>
<div class="info">
<h4 class="filename">
<span data-find-file-name><?= e(ltrim($value, '/')) ?></span>
</h4>
</div>
<div class="meta">
<a href="javascript:;" class="find-remove-button">
<i class="icon-times"></i>
</a>
</div>
</div>
<!-- Empty message -->
<div class="find-empty-message">
<span class="text-muted"><?= $prompt ?></span>
</div>
<!-- Data locker -->
<input
type="hidden"
name="<?= $field->getName() ?>"
id="<?= $field->getId() ?>"
value="<?= e($value) ?>"
data-find-value
/>
</div>

View File

@@ -0,0 +1,56 @@
<div
id="<?= $this->getId() ?>"
class="
field-mediafinder
style-image-single
is-image
<?php if ($value): ?>
is-populated
<?php endif ?>
<?php if ($this->previewMode): ?>
is-preview
<?php endif ?>
"
data-control="mediafinder"
data-mediafinder-mode="image"
data-thumbnail-width="<?= $imageWidth ?: '0' ?>"
data-thumbnail-height="<?= $imageHeight ?: '0' ?>"
<?= $field->getAttributes() ?>
>
<!-- Find Button -->
<a href="javascript:;" class="find-button">
<span class="find-button-icon wn-icon-image"></span>
</a>
<!-- Existing value -->
<div class="find-object">
<div class="icon-container">
<img data-find-image src="<?= $imageUrl ?>" alt="" />
</div>
<?php if (!$imageExists && !empty($imageUrl)): ?>
<p data-find-error class="help-block">
<?= e(trans('backend::lang.mediafinder.no_image')) ?>
</p>
<?php endif; ?>
<div class="info">
<h4 class="filename">
<span data-find-file-name><?= e(ltrim($value, '/')) ?></span>
<a href="javascript:;" class="find-remove-button">
<i class="icon-times"></i>
</a>
</h4>
</div>
</div>
<!-- Data locker -->
<input
type="hidden"
name="<?= $field->getName() ?>"
id="<?= $field->getId() ?>"
value="<?= e($value) ?>"
data-find-value
/>
</div>

View File

@@ -0,0 +1,24 @@
<?php if ($this->previewMode && !$value): ?>
<span class="form-control" disabled="disabled"><?= e(trans('backend::lang.form.preview_no_media_message')) ?></span>
<?php else: ?>
<?php
switch ($mode) {
case 'image':
echo $this->makePartial('image_single');
break;
case 'video':
case 'audio':
case 'document':
echo $this->makePartial('file_single', ['mode' => $mode]);
break;
case 'file':
case 'all':
default:
echo $this->makePartial('file_single', ['mode' => 'all']);
}
?>
<?php endif ?>