pixl-xyapp 2.1.11 → 2.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -293,7 +293,7 @@ The library provides CSS styles and JavaScript functions for creating data table
293
293
  </table>
294
294
  ```
295
295
 
296
- In addition to the CSS, a pagination system is provided, to assist you with generating tables from a large dataset that have pagination links built-in. The function to call is `this.getPaginatedTable()` and is available in the `Page` base class. It returns the final rendered HTML for the page.
296
+ In addition to the CSS, a pagination system is provided, to assist you with generating tables from a large dataset that have pagination links built-in. The function to call is `this.getPaginatedGrid()` and is available in the `Page` base class. It returns the final rendered HTML for the page.
297
297
 
298
298
  To use it, you'll need to provide an object containing the following pieces of information:
299
299
 
@@ -322,7 +322,7 @@ Here is an example:
322
322
  { name: 'Rhubarb', color: 'Purple', size: '2ft', quantity: 190, price: '$3.99', created: 1441724876 }
323
323
  ];
324
324
 
325
- var html = this.getPaginatedTable({
325
+ var html = this.getPaginatedGrid({
326
326
  cols: cols,
327
327
  rows: rows,
328
328
  data_type: 'vegetable',
@@ -343,7 +343,7 @@ Here is an example:
343
343
  });
344
344
  ```
345
345
 
346
- So the idea here is, we have a dataset of 10 items total, but we are only showing 5 items per page. So we have an array of 5 items in `rows`, but we're specifying the `total` as 10, and `offset` as 0 (first page). Based on this, the `getPaginatedTable()` will generate the proper pagination links.
346
+ So the idea here is, we have a dataset of 10 items total, but we are only showing 5 items per page. So we have an array of 5 items in `rows`, but we're specifying the `total` as 10, and `offset` as 0 (first page). Based on this, the `getPaginatedGrid()` will generate the proper pagination links.
347
347
 
348
348
  Your callback is fired once per row, and is passed the current row (array element from `rows`), and the localized index in `idx` (starts from `0` regardless of `offset`). Your function should return an array of values which should match up with the `cols`, and each will be stuffed into a `<TD>` element.
349
349
 
package/css/base.css CHANGED
@@ -185,6 +185,9 @@ button.link {
185
185
  margin: 0;
186
186
  font: inherit;
187
187
  }
188
+ button.link.icon_pad i.mdi {
189
+ padding-right: 5px;
190
+ }
188
191
 
189
192
  a, .link {
190
193
  color: var(--link-color);
@@ -1311,9 +1314,27 @@ body.dark .dialog .multiselect > .item {
1311
1314
  background: rgba(255, 255, 255, 0.1);
1312
1315
  }
1313
1316
 
1314
- .multiselect > .item > i.mdi-close:hover:before {
1317
+ .multiselect > .item > button.mdi {
1318
+ background: none;
1319
+ border: none;
1320
+ padding: 0;
1321
+ margin: 0;
1322
+ font: inherit;
1323
+ cursor: pointer;
1324
+ }
1325
+
1326
+ .multiselect > .item > button.mdi:before {
1327
+ transform: scale(1.20);
1328
+ }
1329
+
1330
+ .multiselect > .item > button.mdi:focus-visible {
1331
+ outline: 2px solid var(--theme-color);
1332
+ outline-offset: -2px;
1333
+ }
1334
+
1335
+ .multiselect > .item > button.mdi-close:hover:before {
1315
1336
  content: "\F0159"; /* mdi-close-circle */
1316
- color: var(--theme-color);
1337
+ color: var(--red);
1317
1338
  }
1318
1339
 
1319
1340
  .multiselect > .item.inherited {
@@ -2535,6 +2556,9 @@ div.dropzone.drag > div.dz_bar {
2535
2556
  overflow: hidden;
2536
2557
  text-overflow: ellipsis;
2537
2558
  }
2559
+ .data_grid_pagination button.link {
2560
+ text-transform: uppercase;
2561
+ }
2538
2562
  .data_grid_pagination i.mdi:before {
2539
2563
  transform: scale(1.5);
2540
2564
  }
package/js/base.js CHANGED
@@ -8,6 +8,11 @@ var app = {
8
8
  secure: !!location.protocol.match(/^https/i),
9
9
  retina: (window.devicePixelRatio > 1),
10
10
  mobile: !!navigator.userAgent.match(/(iOS|iPhone|iPad|Android)/),
11
+ os: {
12
+ mac: !!navigator.userAgent.match(/(Macintosh|Mac OS X|macOS)/),
13
+ win: !!navigator.userAgent.match(/(Windows)/),
14
+ linux: !!navigator.userAgent.match(/(Linux)/)
15
+ },
11
16
  base_api_url: '/api',
12
17
  plain_text_post: false,
13
18
  prefs: {},
@@ -163,6 +168,7 @@ var app = {
163
168
  var id = this.page_manager.current_page_id;
164
169
  var page = this.page_manager.find(id);
165
170
  if (page && page.onKeyDown) page.onKeyDown(event);
171
+ else if (app.onKeyDown) app.onKeyDown(event);
166
172
  }
167
173
  else if (app.onKeyDown) app.onKeyDown(event);
168
174
  },
package/js/page.js CHANGED
@@ -493,161 +493,6 @@ window.Page = class Page {
493
493
  $(elem).closest('.form_row_range').find('input').val(value);
494
494
  }
495
495
 
496
- getPaginatedTable() {
497
- // get html for paginated table
498
- // dual-calling convention: (resp, cols, data_type, callback) or (args)
499
- var args = null;
500
- if (arguments.length == 1) {
501
- // custom args calling convention
502
- args = arguments[0];
503
-
504
- // V2 API
505
- if (!args.resp && args.rows && args.total) {
506
- args.resp = {
507
- rows: args.rows,
508
- list: { length: args.total }
509
- };
510
- }
511
- }
512
- else {
513
- // classic calling convention
514
- args = {
515
- resp: arguments[0],
516
- cols: arguments[1],
517
- data_type: arguments[2],
518
- callback: arguments[3],
519
- limit: this.args.limit,
520
- offset: this.args.offset || 0
521
- };
522
- }
523
-
524
- var resp = args.resp;
525
- var cols = args.cols;
526
- var data_type = args.data_type;
527
- var callback = args.callback;
528
- var cpl = args.pagination_link || '';
529
- var html = '';
530
-
531
- // pagination header
532
- html += '<div class="pagination">';
533
- html += '<table cellspacing="0" cellpadding="0" border="0" width="100%"><tr>';
534
-
535
- var results = {
536
- limit: args.limit,
537
- offset: args.offset || 0,
538
- total: resp.list.length
539
- };
540
-
541
- var num_pages = Math.floor( results.total / results.limit ) + 1;
542
- if (results.total % results.limit == 0) num_pages--;
543
- var current_page = Math.floor( results.offset / results.limit ) + 1;
544
-
545
- html += '<td align="left" width="33%">';
546
- html += commify(results.total) + ' ' + pluralize(data_type, results.total) + ' found';
547
- html += '</td>';
548
-
549
- html += '<td align="center" width="34%">';
550
- if (num_pages > 1) html += 'Page ' + commify(current_page) + ' of ' + commify(num_pages);
551
- else html += '&nbsp;';
552
- html += '</td>';
553
-
554
- html += '<td align="right" width="33%">';
555
-
556
- if (num_pages > 1) {
557
- // html += 'Page: ';
558
- if (current_page > 1) {
559
- if (cpl) {
560
- html += '<span class="link" onClick="'+cpl+'('+Math.floor((current_page - 2) * results.limit)+')">&laquo; Prev</span>';
561
- }
562
- else {
563
- html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
564
- offset: (current_page - 2) * results.limit
565
- })) + '">&laquo; Prev</a>';
566
- }
567
- }
568
- html += '&nbsp;&nbsp;&nbsp;';
569
-
570
- var start_page = current_page - 4;
571
- var end_page = current_page + 5;
572
-
573
- if (start_page < 1) {
574
- end_page += (1 - start_page);
575
- start_page = 1;
576
- }
577
-
578
- if (end_page > num_pages) {
579
- start_page -= (end_page - num_pages);
580
- if (start_page < 1) start_page = 1;
581
- end_page = num_pages;
582
- }
583
-
584
- for (var idx = start_page; idx <= end_page; idx++) {
585
- if (idx == current_page) {
586
- html += '<b>' + commify(idx) + '</b>';
587
- }
588
- else {
589
- if (cpl) {
590
- html += '<span class="link" onClick="'+cpl+'('+Math.floor((idx - 1) * results.limit)+')">' + commify(idx) + '</span>';
591
- }
592
- else {
593
- html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
594
- offset: (idx - 1) * results.limit
595
- })) + '">' + commify(idx) + '</a>';
596
- }
597
- }
598
- html += '&nbsp;';
599
- }
600
-
601
- html += '&nbsp;&nbsp;';
602
- if (current_page < num_pages) {
603
- if (cpl) {
604
- html += '<span class="link" onClick="'+cpl+'('+Math.floor((current_page + 0) * results.limit)+')">Next &raquo;</span>';
605
- }
606
- else {
607
- html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
608
- offset: (current_page + 0) * results.limit
609
- })) + '">Next &raquo;</a>';
610
- }
611
- }
612
- } // more than one page
613
- else {
614
- html += 'Page 1 of 1';
615
- }
616
- html += '</td>';
617
- html += '</tr></table>';
618
- html += '</div>';
619
-
620
- html += '<div style="margin-top:5px; overflow-x:auto;">';
621
-
622
- var tattrs = args.attribs || {};
623
- if (!tattrs.class) tattrs.class = 'data_table ellip';
624
- if (!tattrs.width) tattrs.width = '100%';
625
- html += '<table ' + compose_attribs(tattrs) + '>';
626
-
627
- html += '<tr><th>' + cols.join('</th><th>').replace(/\s+/g, '&nbsp;') + '</th></tr>';
628
-
629
- for (var idx = 0, len = resp.rows.length; idx < len; idx++) {
630
- var row = resp.rows[idx];
631
- var tds = callback(row, idx);
632
- if (tds) {
633
- html += '<tr' + (tds.className ? (' class="'+tds.className+'"') : '') + '>';
634
- html += '<td>' + tds.join('</td><td>') + '</td>';
635
- html += '</tr>';
636
- }
637
- } // foreach row
638
-
639
- if (!resp.rows.length) {
640
- html += '<tr><td colspan="'+cols.length+'" align="center" style="padding-top:10px; padding-bottom:10px; font-weight:bold;">';
641
- html += 'No '+pluralize(data_type)+' found.';
642
- html += '</td></tr>';
643
- }
644
-
645
- html += '</table>';
646
- html += '</div>';
647
-
648
- return html;
649
- }
650
-
651
496
  getPaginatedGrid() {
652
497
  // get html for paginated grid
653
498
  // multi-calling convention: (resp, cols, data_type, callback), or (args, callback), or (args)
@@ -717,12 +562,13 @@ window.Page = class Page {
717
562
  // html += 'Page: ';
718
563
  if (current_page > 1) {
719
564
  if (cpl) {
720
- html += '<span class="link" onClick="'+cpl+'('+Math.floor((current_page - 2) * results.limit)+')"><i class="mdi mdi-chevron-left"></i>&nbsp;Prev</span>';
565
+ var click = cpl + '(' + Math.floor((current_page - 2) * results.limit) + ')';
566
+ html += '<button class="link" ' + (args.primary ? 'id="btn_nav_prev"' : '') + ' onClick="' + click + '"><i class="mdi mdi-chevron-left"></i>&nbsp;Prev</button>';
721
567
  }
722
568
  else {
723
569
  html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
724
570
  offset: (current_page - 2) * results.limit
725
- })) + '">&laquo; Prev</a>';
571
+ })) + '" ' + (args.primary ? 'id="btn_nav_prev"' : '') + '><i class="mdi mdi-chevron-left"></i>&nbsp;Prev</a>';
726
572
  }
727
573
  }
728
574
  html += '&nbsp;&nbsp;&nbsp;';
@@ -748,7 +594,7 @@ window.Page = class Page {
748
594
  }
749
595
  else {
750
596
  if (cpl) {
751
- html += '<span class="link" onClick="'+cpl+'('+Math.floor((idx - 1) * results.limit)+')">' + commify(idx) + '</span>';
597
+ html += '<button class="link" onClick="'+cpl+'('+Math.floor((idx - 1) * results.limit)+')">' + commify(idx) + '</button>';
752
598
  }
753
599
  else {
754
600
  html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
@@ -763,12 +609,13 @@ window.Page = class Page {
763
609
 
764
610
  if (current_page < num_pages) {
765
611
  if (cpl) {
766
- html += '<span class="link" onClick="'+cpl+'('+Math.floor((current_page + 0) * results.limit)+')">Next&nbsp;<i class="mdi mdi-chevron-right"></i></span>';
612
+ var click = cpl + '(' + Math.floor((current_page + 0) * results.limit) + ')';
613
+ html += '<button class="link" ' + (args.primary ? 'id="btn_nav_next"' : '') + ' onClick="' + click + '">Next&nbsp;<i class="mdi mdi-chevron-right"></i></button>';
767
614
  }
768
615
  else {
769
616
  html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
770
617
  offset: (current_page + 0) * results.limit
771
- })) + '">Next &raquo;</a>';
618
+ })) + '" ' + (args.primary ? 'id="btn_nav_next"' : '') + '>Next&nbsp;<i class="mdi mdi-chevron-right"></i></a>';
772
619
  }
773
620
  }
774
621
  } // more than one page
package/js/select.js CHANGED
@@ -358,7 +358,7 @@ var MultiSelect = {
358
358
  }
359
359
  else if (opt.selected) {
360
360
  // item is selected
361
- var html = '<i class="mdi mdi-close">&nbsp;</i>';
361
+ var html = '<button class="mdi mdi-close" aria-label="Remove Item: ' + encode_attrib_entities(opt.label) + '">&nbsp;</button>';
362
362
  if (opt.getAttribute && opt.getAttribute('data-icon')) {
363
363
  html += '<i class="mdi mdi-' + opt.getAttribute('data-icon') + '">&nbsp;</i>';
364
364
  }
@@ -372,8 +372,10 @@ var MultiSelect = {
372
372
  if (num_sel) $ms.append( '<div class="clear"></div>' );
373
373
  else $ms.append( '<div class="placeholder">' + ($this.attr('placeholder') || 'Click to select...') + '</div>' );
374
374
 
375
- $ms.find('div.item > i.mdi-close').on('click', function(e) {
375
+ $ms.find('div.item > button.mdi-close').on('click keydown', function(e) {
376
376
  // user clicked on the 'X' -- remove this item and redraw
377
+ if (e.key && (e.key != 'Enter') && (e.key != ' ')) return;
378
+
377
379
  var $item = $(this).parent();
378
380
  var value = $item.data('value');
379
381
  for (var idx = 0, len = self.options.length; idx < len; idx++) {
@@ -411,7 +413,7 @@ var MultiSelect = {
411
413
  $this.on('redraw', redraw);
412
414
 
413
415
  // allow keyboard to open menu
414
- $ms.on('keydown', function(event) {
416
+ $ms.on('keypress', function(event) {
415
417
  if ((event.key == 'Enter') || (event.key == ' ')) {
416
418
  $ms.click();
417
419
  event.preventDefault();
@@ -776,7 +778,7 @@ var TextSelect = {
776
778
  for (var idx = 0, len = self.options.length; idx < len; idx++) {
777
779
  var opt = self.options[idx];
778
780
  var $item = $('<div class="item"></div>').data('value', opt.value).html(
779
- '<i class="mdi mdi-close">&nbsp;</i>' + opt.label
781
+ '<button class="mdi mdi-close" aria-label="Remove Item: ' + encode_attrib_entities(opt.label) + '">&nbsp;</button>' + opt.label
780
782
  );
781
783
  $ms.append( $item );
782
784
  num_sel++;
@@ -785,8 +787,10 @@ var TextSelect = {
785
787
  if (num_sel) $ms.append( '<div class="clear"></div>' );
786
788
  else $ms.append( '<div class="placeholder">' + ($this.attr('placeholder') || 'Click to add...') + '</div>' );
787
789
 
788
- $ms.find('div.item > i').on('click', function(e) {
790
+ $ms.find('div.item > button.mdi').on('click keydown', function(e) {
789
791
  // user clicked on the 'X' -- remove this item and redraw
792
+ if (e.key && (e.key != 'Enter') && (e.key != ' ')) return;
793
+
790
794
  var $item = $(this).parent();
791
795
  var value = $item.data('value');
792
796
 
@@ -809,7 +813,7 @@ var TextSelect = {
809
813
  $this.on('redraw', redraw);
810
814
 
811
815
  // allow keyboard to open menu
812
- $ms.on('keydown', function(event) {
816
+ $ms.on('keypress', function(event) {
813
817
  if ((event.key == 'Enter') || (event.key == ' ')) {
814
818
  $ms.click();
815
819
  event.preventDefault();
@@ -946,3 +950,166 @@ var TextSelect = {
946
950
  }
947
951
 
948
952
  }; // TextSelect
953
+
954
+ var KeySelect = {
955
+
956
+ init: function(sel) {
957
+ // initialize all key-selects based on selector
958
+ $(sel).each( function() {
959
+ var self = this;
960
+ var $this = $(this);
961
+ $this.css('display', 'none').attr({ 'aria-hidden': true, 'tabindex': '-1' });
962
+
963
+ var $ms = $('<div class="multiselect text" role="button" tabindex="0"></div>');
964
+ $this.after( $ms );
965
+
966
+ var redraw = function() {
967
+ // render contents of visible multiselect div
968
+ var num_sel = 0;
969
+ $ms.empty();
970
+ $ms.append('<div class="select_chevron mdi mdi-plus"></div>');
971
+
972
+ for (var idx = 0, len = self.options.length; idx < len; idx++) {
973
+ var opt = self.options[idx];
974
+ var $item = $('<div class="item"></div>').data('value', opt.value).html(
975
+ '<button class="mdi mdi-close" aria-label="Remove Key: ' + encode_attrib_entities(opt.label) + '">&nbsp;</button>' + opt.label
976
+ );
977
+ $ms.append( $item );
978
+ num_sel++;
979
+ }
980
+
981
+ if (num_sel) $ms.append( '<div class="clear"></div>' );
982
+ else $ms.append( '<div class="placeholder">' + ($this.attr('placeholder') || 'Click to add...') + '</div>' );
983
+
984
+ $ms.find('div.item > button.mdi').on('click keydown', function(e) {
985
+ // user clicked on the 'X' -- remove this item and redraw
986
+ if (e.key && (e.key != 'Enter') && (e.key != ' ')) return;
987
+
988
+ var $item = $(this).parent();
989
+ var value = $item.data('value');
990
+
991
+ var idx = find_object_idx( self.options, { value: value } );
992
+ self.options.remove( idx );
993
+
994
+ $this.trigger('change');
995
+ e.stopPropagation();
996
+ e.preventDefault();
997
+ return false;
998
+ });
999
+ }; // redraw
1000
+
1001
+ redraw();
1002
+
1003
+ // also trigger a redraw if the underlying hidden select changes
1004
+ $this.on('change', redraw);
1005
+
1006
+ // also expose redraw as a custom event that can be triggered
1007
+ $this.on('redraw', redraw);
1008
+
1009
+ // allow keyboard to open menu
1010
+ $ms.on('keypress', function(event) {
1011
+ if ((event.key == 'Enter') || (event.key == ' ')) {
1012
+ $ms.click();
1013
+ event.preventDefault();
1014
+ }
1015
+ } );
1016
+
1017
+ $ms.on('click', function() {
1018
+ // create popover dialog for adding new items
1019
+ var html = '';
1020
+ if ($ms.hasClass('disabled')) return;
1021
+
1022
+ html += '<div class="sel_dialog_label">' + ($this.attr('title') || 'Add New Item') + '</div>';
1023
+ html += '<div class="sel_dialog_search_container">';
1024
+ html += '<input type="hidden" id="fe_sel_dialog_key" value=""/>';
1025
+ html += '<input type="text" id="fe_sel_dialog_text" class="sel_dialog_search" style="border-radius:2px;" autocomplete="off" value=""/>';
1026
+ html += '<div class="sel_dialog_search_icon"><i class="mdi mdi-' + ($this.attr('icon') || 'plus') + '"></i></div>';
1027
+ html += '</div>';
1028
+
1029
+ if ($this.attr('description')) {
1030
+ html += '<div class="sel_dialog_caption">' + $this.attr('description') + '</div>';
1031
+ }
1032
+
1033
+ html += '<div class="sel_dialog_button_container">';
1034
+ html += '<div class="button" id="btn_sel_dialog_cancel">Cancel</div>';
1035
+ html += '<div class="button primary" id="btn_sel_dialog_add">' + ($this.attr('confirm') || 'Add Hot Key') + '</div>';
1036
+ html += '</div>';
1037
+
1038
+ Popover.attach( $ms, '<div style="padding:15px;">' + html + '</div>', $this.data('shrinkwrap') || false );
1039
+
1040
+ var doAdd = function() {
1041
+ app.clearError();
1042
+
1043
+ var value = $('#fe_sel_dialog_key').val();
1044
+ var label = $('#fe_sel_dialog_text').val();
1045
+
1046
+ if (!value.length || find_object(self.options, { value: value })) {
1047
+ Popover.detach();
1048
+ return;
1049
+ }
1050
+
1051
+ // add new item
1052
+ var opt = new Option( label, value );
1053
+ opt.selected = true;
1054
+ self.options[ self.options.length ] = opt;
1055
+
1056
+ Popover.detach();
1057
+ $this.trigger('change');
1058
+ }; // doAdd
1059
+
1060
+ $('#btn_sel_dialog_cancel').on('click', function() { Popover.detach(); });
1061
+ $('#btn_sel_dialog_add').on('click', function() { doAdd(); });
1062
+
1063
+ var $input = $('#fe_sel_dialog_text').focus().on('keydown', function(event) {
1064
+ // capture keydown
1065
+ if ((event.keyCode == 13) && this.value.length) {
1066
+ event.preventDefault();
1067
+ event.stopPropagation();
1068
+ doAdd();
1069
+ }
1070
+
1071
+ var key_id = KeySelect.getKeyID(event);
1072
+ if (!key_id) return;
1073
+
1074
+ event.preventDefault();
1075
+ event.stopPropagation();
1076
+
1077
+ $('#fe_sel_dialog_key').val( key_id );
1078
+ $('#fe_sel_dialog_text').val( KeySelect.getkeyLabel(key_id) );
1079
+ });
1080
+
1081
+ // highlight multiselect field under us
1082
+ $ms.addClass('selected');
1083
+ Popover.onDetach = function() {
1084
+ $ms.removeClass('selected').focus();
1085
+ };
1086
+ }); // click
1087
+
1088
+ }); // forach elem
1089
+ },
1090
+
1091
+ getKeyID(event) {
1092
+ // get get ID based on event
1093
+ // ignore modifiers by themselves
1094
+ if (event.key.match(/^(Shift|Control|Alt|Meta)$/)) return '';
1095
+
1096
+ var parts = [];
1097
+ if (event.shiftKey) parts.push('Shift');
1098
+ if (event.ctrlKey) parts.push('Control');
1099
+ if (event.altKey) parts.push('Alt');
1100
+ if (event.metaKey) parts.push('Meta');
1101
+ if (!parts.includes(event.code)) parts.push( event.code );
1102
+
1103
+ return parts.join('+');
1104
+ },
1105
+
1106
+ getkeyLabel(key_id, glue = '+') {
1107
+ // get formatted label based on key id
1108
+ var os = app.os;
1109
+ return key_id.split(/\+/).map( function(key) {
1110
+ if (key == 'Meta') return os.mac ? 'Command' : (os.win ? 'Windows' : 'Super');
1111
+ else return key.replace(/^(Key|Digit)/, '');
1112
+ } ).join(glue);
1113
+ }
1114
+
1115
+ }; // KeySelect
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pixl-xyapp",
3
- "version": "2.1.11",
4
- "description": "A theme for xyOps.",
3
+ "version": "2.1.14",
4
+ "description": "Front-end web application and theme for xyOps.",
5
5
  "author": "Joseph Huckaby <jhuckaby@pixlcore.com>",
6
6
  "homepage": "https://github.com/pixlcore/pixl-xyapp",
7
7
  "license": "BSD-3-Clause",