Compare commits

...

10 commits

Author SHA1 Message Date
9d98b69e31 feat: Features for table screen
- Add active state for rows
- Add addition of rows
2026-02-14 19:17:46 +01:00
9bf97019ca docs: More details for JS part 2026-02-14 18:54:14 +01:00
5092c1680d feat: Add capitalizeFirstLetter() to app 2026-02-14 18:53:48 +01:00
e5516c81f1 chore: Display jshint option for curly braces 2026-02-14 18:44:45 +01:00
15a9b83e80 feat: Content and interaction for table screen
- Add getRandomFormattedString(), toggleColumn() and convertToRomanNumeral() to app
- Update hippie styles
- Add button to toggle index column
- Add select group to autofill numbering
2026-02-14 13:26:06 +01:00
b1c7f4100e feat: Unify format for IDs 2026-02-14 12:18:07 +01:00
759283fd9f docs: Update README and TODO
- Better structure for TODOs
- Add information about JavaScript to README
2026-02-14 12:16:55 +01:00
0a5bc191d3 chore: Set 11 as ECMAScript version in jshint 2026-02-14 12:15:18 +01:00
498f59a939 feat: Replace ui cli and tui with liquid versions 2026-02-13 19:59:22 +01:00
bbd1a9674e feat: Replace ui explorer with liquid template
- Explorer screen uses app.liquid layout instead of _app_frame.njk
- Rename to just Explorer
2026-02-13 19:23:10 +01:00
16 changed files with 565 additions and 376 deletions

View file

@ -1,9 +1,9 @@
{
"bitwise": true,
"curly": true,
"curly": false,
"devel": true,
"eqeqeq": true,
"esversion": 9,
"esversion": 11,
"forin": true,
"freeze": true,
"jquery": true,

View file

@ -44,9 +44,25 @@ Everything has its default style.
CSS classes follow a naming scheme of `<object>_<description>`.
- *Object* usually is the name of the HTML element. If it is not a elemtn directly it is the thing which receives the styling
- *Object* usually is the name of the HTML element. If it is not a element directly it is the thing which receives the styling
- *Description* is a name of the style e.g. what it does, how it looks
### JavaScript (JS)
The codebase uses ECMAScript 2023 Language Specification (14th edition).
However, currently the JSHint configuration only allows 11 as highest version.
ID values are written in *camelCase*. The scheme further uses parts for specific contexts.
If the ID is for an interactive element the first part is an abbreviation of the action.
- `add` - Add
- `qry` - Query
- `rmv` - Remove
- `set` - Set
- `slt` - Select
- `tgl` - Toggle
## Versioning
This project uses [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://quelltext.interaktionsweise.de/interaktionsweise/hippie/tags).

12
TODO.md
View file

@ -11,9 +11,15 @@
- Add build process for normalize.css from github.com/necolas/normalize.css
- Add build process for bourbon from https://github.com/thoughtbot/bourbon
# CSS
- Check style for every basic element
- Uniform spelling of classes and identifiers
# Content
- Complete *Intro* with description for all basic elements, according to [HTML Standard - The elements of HTML](https://html.spec.whatwg.org/multipage/semantics.html#semantics)
- *Intro*
- Complete with description for all basic elements, according to [HTML Standard - The elements of HTML](https://html.spec.whatwg.org/multipage/semantics.html#semantics)
- Sections
- [x] body
- [x] article
@ -126,5 +132,5 @@
- [ ] template
- [ ] slot
- [ ] canvas
- Check style for every basic element
- Uniform spelling of classes and identifiers
- *Clock*
- Add overlays to better distinguish day, week and year

View file

@ -351,12 +351,12 @@ function checkButtonAndTarget(event, element, button = 0) {
function getClosestEdgeToElement(element) {
'use strict';
const rect = element.getBoundingClientRect();
const bounding = element.getBoundingClientRect();
const distances = {
top: rect.top,
right: window.innerWidth - rect.right,
bottom: window.innerHeight - rect.bottom,
left: rect.left
top: bounding.top,
right: window.innerWidth - bounding.right,
bottom: window.innerHeight - bounding.bottom,
left: bounding.left
};
return Object.keys(distances).reduce((a, b) => distances[a] < distances[b] ? a : b);
@ -459,6 +459,70 @@ function getRandomColor() {
return color;
}
function getRandomFormattedString(chars = 2, digits = 6, separator = '-') {
const getRandomUppercase = () => String.fromCharCode(Math.floor(Math.random() * 26) + 65);
const getRandomDigit = () => Math.floor(Math.random() * 10);
let string = '';
for (let i = 0; i < chars; i++) {
string += getRandomUppercase();
}
string += separator;
for (let i = 0; i < digits; i++) {
string += getRandomDigit();
}
return string;
}
function toggleColumn(table, index) {
const rows = table.rows;
const isHidden = rows[0].cells[index].classList.contains('di_none');
for (let i = 0; i < rows.length; i++) {
const cell = rows[i].cells[index];
if (isHidden) {
cell.classList.remove('di_none');
} else {
cell.classList.add('di_none');
}
}
}
function convertToRomanNumeral(num) {
const romanNumeralMap = [
{value: 1000, numeral: 'M'},
{value: 900, numeral: 'CM'},
{value: 500, numeral: 'D'},
{value: 400, numeral: 'CD'},
{value: 100, numeral: 'C'},
{value: 90, numeral: 'XC'},
{value: 50, numeral: 'L'},
{value: 40, numeral: 'XL'},
{value: 10, numeral: 'X'},
{value: 9, numeral: 'IX'},
{value: 5, numeral: 'V'},
{value: 4, numeral: 'IV'},
{value: 1, numeral: 'I'}
];
let result = '';
for (let i = 0; i < romanNumeralMap.length; i++) {
while (num >= romanNumeralMap[i].value) {
result += romanNumeralMap[i].numeral;
num -= romanNumeralMap[i].value;
}
}
return result;
}
function capitalizeFirstLetter(text) {
return text.charAt(0).toUpperCase() + text.slice(1);
}
// CONCEPTS
// NOTE: Benutzt private Zuweisungen

View file

@ -11,7 +11,7 @@ tags:
<main>
<canvas id="clock" width="512" height="512"></canvas>
<p>
<button id="toggleFormat">12-Stunden-Format</button>
<button id="tglFormat">12-Stunden-Format</button>
</p>
</main>
{% endblock %}
@ -27,9 +27,9 @@ tags:
const colorDayOfMonth = getComputedStyle(document.documentElement).getPropertyValue('--clock-color-orange').trim();
const colorMonth = getComputedStyle(document.documentElement).getPropertyValue('--clock-color-pink').trim();
document.getElementById('toggleFormat').addEventListener('click', () => {
document.getElementById('tglFormat').addEventListener('click', () => {
is24HourFormat = !is24HourFormat;
document.getElementById('toggleFormat').textContent = is24HourFormat ? '12-Stunden-Format' : '24-Stunden-Format';
document.getElementById('tglFormat').textContent = is24HourFormat ? '12-Stunden-Format' : '24-Stunden-Format';
});
function drawRings(seconds, minutes, hours, dayOfWeek, dayOfMonth, month, daysInCurrentMonth) {

View file

@ -11,7 +11,7 @@ tags:
{% block body %}
<main>
<form id="www-search" class="flex inline" action="https://duckduckgo.com/">
<input id="query" class="input_io" name="q" placeholder="Suchbegriff" type="text" required/>
<input id="qrySearch" class="input_io" name="q" placeholder="Suchbegriff" type="text" required/>
<input class="button_io" value="Suchen" type="submit"/>
</form>
<div class="blocks">
@ -69,7 +69,7 @@ tags:
document.getElementById('www-search').addEventListener('submit', function (e) {
e.preventDefault();
const query = document.getElementById('query').value.trim();
const query = document.getElementById('qrySearch').value.trim();
if (!query) return;

View file

@ -0,0 +1,60 @@
---
title: CLI
tags:
- ui
---
{% assign bodyClass = "body_cli" -%}
{% layout "hippie/app.liquid" %}
{% block body %}
<div id="cli">
<div id="history">
<pre>Previous prompt</pre>
</div>
<div id="line">
<textarea name="prefix" id="prefix" rows="1" cols="1" readonly>&gt;</textarea>
<textarea name="prompt" id="prompt" rows="1" autofocus></textarea>
</div>
</div>
{% endblock %}
{%- block script %}
{{ block.super -}}
<script>
function resizeTextArea(textarea) {
const {style, value} = textarea;
style.height = style.minHeight = 'auto';
style.minHeight = `${Math.min(textarea.scrollHeight + 4, parseInt(textarea.style.maxHeight))}px`;
style.height = `${textarea.scrollHeight}px`;
}
const textarea = document.getElementById('prompt');
document
.body
.addEventListener('click', () => {
textarea.focus();
});
textarea.addEventListener('input', () => {
resizeTextArea(textarea);
});
window.addEventListener("keydown", (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
const p = document.createElement("pre");
p.textContent = event.target.value;
document
.getElementById("history")
.appendChild(p);
// window.scrollTo(0, document.body.scrollHeight);
event.target.value = '';
}
});
</script>
{% endblock %}

View file

@ -1,62 +0,0 @@
---
title: CLI
tags:
- ui
---
{% set pageId = page.fileSlug %}
{% set bodyClass = "body_cli" %}
{% extends "hippie/_app_frame.njk" %}
{% block body %}
<div id="cli">
<div id="history">
<pre>Previous prompt</pre>
</div>
<div id="line">
<textarea name="prefix" id="prefix" rows="1" cols="1" readonly>&gt;</textarea>
<textarea name="prompt" id="prompt" rows="1" autofocus></textarea>
</div>
</div>
{% endblock %}
{%- block script %}
{{ super() }}
<script>
function resizeTextArea(textarea) {
const {style, value} = textarea;
style.height = style.minHeight = 'auto';
style.minHeight = `${Math.min(textarea.scrollHeight + 4, parseInt(textarea.style.maxHeight))}px`;
style.height = `${textarea.scrollHeight}px`;
}
const textarea = document.getElementById('prompt');
document
.body
.addEventListener('click', () => {
textarea.focus();
});
textarea.addEventListener('input', () => {
resizeTextArea(textarea);
});
window.addEventListener("keydown", (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
const p = document.createElement("pre");
p.textContent = event.target.value;
document
.getElementById("history")
.appendChild(p);
// window.scrollTo(0, document.body.scrollHeight);
event.target.value = '';
}
});
</script>
{% endblock %}

View file

@ -0,0 +1,145 @@
---
title: Explorer
tags:
- ui
---
{% assign bodyClass = "body_frame" -%}
{% layout "hippie/app.liquid" %}
{% block body %}
{% render 'hippie/partials/frame-header.liquid' %}
<main class="io">
<aside class="io">
<nav>
<span>location-pane</span>
<ul class="vertical">
<li>
<a class="a_button" href="">
<span>Start/Home</span>
<i class="bi bi-pin-fill"></i>
</a>
</li>
<li>
<a class="a_button" href="">
<span>System</span>
<i class="bi bi-pin-fill"></i>
</a>
</li>
</ul>
</nav>
<hr>
<nav>
<ul class="vertical">
<li>
<a class="a_button" href="">
<span>💽 disc link</span>
</a>
</li>
<li>
<a class="a_button" href="">
<span>📁 folder link</span>
</a>
</li>
<li>
<a class="a_button" href="">
<span>🔗 location link</span>
</a>
</li>
</ul>
</nav>
</aside>
<section>
<header class="io">
<nav>
<button title="back">
<i class="bi bi-caret-left"></i>
</button>
<button title="up">
<i class="bi bi-caret-up"></i>
</button>
<button title="forward">
<i class="bi bi-caret-right"></i>
</button>
<button title="reload">
<i class="bi bi-arrow-clockwise"></i>
</button>
<input placeholder="//" type="text">
</nav>
<nav>
<span>location-bar</span>
<hr class="vertical">
<button title="search">
<i class="bi bi-search"></i>
</button>
<input placeholder="*" type="text">
</nav>
</header>
<header class="io">
<nav>
<button title="add">
<i class="bi bi-plus-circle"></i>
</button>
<div class="spacer a"></div>
<button title="cut">
<i class="bi bi-scissors"></i>
</button>
<button title="copy">
<i class="bi bi-copy"></i>
</button>
<button title="paste">
<i class="bi bi-clipboard-check-fill"></i>
</button>
<div class="spacer b"></div>
<button title="delete">
<i class="bi bi-trash"></i>
</button>
</nav>
<hr class="vertical">
<nav>
<ul>
<li>
<button title="collapse">
<i class="bi bi-arrows-collapse"></i>
</button>
</li>
<li>
<button title="expand">
<i class="bi bi-arrows-expand-vertical"></i>
</button>
</li>
</ul>
<span>options-bar</span>
</nav>
<nav></nav>
</header>
<div id="content">
<table>
<thead>
<tr>
<th>name</th>
<th>date</th>
<th>type</th>
<th>size</th>
</tr>
</thead>
<tbody>
<tr>
<td>folder</td>
<td>YYYY-MM-DD</td>
<td>folder</td>
<td>4KB</td>
</tr>
<tr>
<td>file</td>
<td>YYYY-MM-DD</td>
<td>folder</td>
<td>1B</td>
</tr>
</tbody>
</table>
</div>
{% render 'hippie/partials/frame-status.liquid' %}
</section>
</main>
{% render 'hippie/partials/frame-mode.liquid' %}
{% endblock %}

View file

@ -1,146 +0,0 @@
---
title: GUI explorer
tags:
- ui
---
{% set pageId = page.fileSlug %}
{% extends "hippie/_app_frame.njk" %}
{% block body %}
{{ io.frameHeader('title-bar') }}
<main class="io">
<aside class="io">
<nav>
<span>location-pane</span>
<ul class="vertical">
<li>
<a class="a_button" href="">
<span>Start/Home</span>
<i class="bi bi-pin-fill"></i>
</a>
</li>
<li>
<a class="a_button" href="">
<span>System</span>
<i class="bi bi-pin-fill"></i>
</a>
</li>
</ul>
</nav>
<hr>
<nav>
<ul class="vertical">
<li>
<a class="a_button" href="">
<span>💽 disc link</span>
</a>
</li>
<li>
<a class="a_button" href="">
<span>📁 folder link</span>
</a>
</li>
<li>
<a class="a_button" href="">
<span>🔗 location link</span>
</a>
</li>
</ul>
</nav>
</aside>
<section>
<header class="io">
<nav>
<button title="back">
<i class="bi bi-caret-left"></i>
</button>
<button title="up">
<i class="bi bi-caret-up"></i>
</button>
<button title="forward">
<i class="bi bi-caret-right"></i>
</button>
<button title="reload">
<i class="bi bi-arrow-clockwise"></i>
</button>
<input placeholder="//" type="text">
</nav>
<nav>
<span>location-bar</span>
<hr class="vertical">
<button title="search">
<i class="bi bi-search"></i>
</button>
<input placeholder="*" type="text">
</nav>
</header>
<header class="io">
<nav>
<button title="add">
<i class="bi bi-plus-circle"></i>
</button>
<div class="spacer a"></div>
<button title="cut">
<i class="bi bi-scissors"></i>
</button>
<button title="copy">
<i class="bi bi-copy"></i>
</button>
<button title="paste">
<i class="bi bi-clipboard-check-fill"></i>
</button>
<div class="spacer b"></div>
<button title="delete">
<i class="bi bi-trash"></i>
</button>
</nav>
<hr class="vertical">
<nav>
<ul>
<li>
<button title="collapse">
<i class="bi bi-arrows-collapse"></i>
</button>
</li>
<li>
<button title="expand">
<i class="bi bi-arrows-expand-vertical"></i>
</button>
</li>
</ul>
<span>options-bar</span>
</nav>
<nav></nav>
</header>
<div id="content">
<table>
<thead>
<tr>
<th>name</th>
<th>date</th>
<th>type</th>
<th>size</th>
</tr>
</thead>
<tbody>
<tr>
<td>folder</td>
<td>YYYY-MM-DD</td>
<td>folder</td>
<td>4KB</td>
</tr>
<tr>
<td>file</td>
<td>YYYY-MM-DD</td>
<td>folder</td>
<td>1B</td>
</tr>
</tbody>
</table>
</div>
{{ io.statusBar() }}
</section>
</main>
{{ io.frameFooter('mode-bar') }}
{% endblock %}

View file

@ -12,9 +12,9 @@ tags:
<section>
<header class="io">
<nav>
<div class="group">
<input id="slider-size" value="5" min="1" max="10" step="1" type="range"/>
<label class="right" for="slider-size">Größe</label>
<div class="group-input">
<input id="setSize" value="5" min="1" max="10" step="1" type="range"/>
<label class="right" for="setSize">Größe</label>
</div>
<button title="details">
<i class="bi bi-layout-sidebar-reverse"></i>&nbsp;Details
@ -36,7 +36,7 @@ tags:
{%- block script %}
{{ block.super -}}
<script>
const sizeSlider = document.getElementById('slider-size');
const sizeSlider = document.getElementById('setSize');
const galleryItems = document.querySelectorAll('.gallery > div');
// Set the default size

View file

@ -12,13 +12,29 @@ tags:
<section>
<header class="io">
<nav>
<button title="add">
<div class="group-input">
<button id="addEntry" title="Add entry">
<i class="bi bi-plus-circle"></i>
</button>
<div class="group-input-wrap"><input id="setScroll" type="checkbox"></div>
<label for="setScroll">Scroll to new entry</label>
</div>
<button id="tglIndex" title="Toggle index column">
<i class="bi bi-hash"></i>
</button>
<div class="group-input">
<select id="sltNum" name="position-number">
<option value="" selected>None</option>
<option value="numeric">123</option>
<option value="latin">ABC</option>
<option value="roman">IVX</option>
</select>
<button id="setPosNum" title="Set numbering" type="button"><i class="bi bi-list-ol"></i></button>
</div>
</nav>
<nav></nav>
</header>
<table>
<table id="content">
<thead>
<tr>
<th scope="col" title="Index">#</th>
@ -31,22 +47,22 @@ tags:
<th scope="col"></th>
</tr>
</thead>
<tbody></tbody>
<tbody id="positions"></tbody>
</table>
{% render 'hippie/partials/frame-status.liquid' %}
</section>
</main>
<template id="default-row">
<template id="rowDefault">
<tr>
<th scope="row"></th>
<td class="io">
<nav>
<button title="drag"><i class="bi bi-grip-horizontal"></i></button>
<button title="expand"><i class="bi bi-arrows-expand-vertical"></i></button>
<button title="Drag"><i class="bi bi-grip-horizontal"></i></button>
<button title="Expand"><i class="bi bi-arrows-expand"></i></button>
</nav>
</td>
<td></td>
<td></td>
<td class="pos-num"></td>
<td class="rigid"></td>
<td></td>
{% comment %}<td class="ellipsis"></td>{% endcomment %}
<td>
@ -55,9 +71,9 @@ tags:
<td class="unit"></td>
<td class="io">
<nav>
<button title="event"><i class="bi bi-calendar-event"></i></button>
<button title="note"><i class="bi bi-journal"></i></button>
<button title="delete"><i class="bi bi-trash"></i></button>
<button title="Event"><i class="bi bi-calendar-event"></i></button>
<button title="Note"><i class="bi bi-journal"></i></button>
<button title="Delete"><i class="bi bi-trash"></i></button>
</nav>
</td>
</tr>
@ -97,13 +113,27 @@ tags:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum facilisis condimentum quam, bibendum tristique odio.'
];
const tbody = document.querySelector('main.io section > table');
const template = document.querySelector('#default-row');
const currencyEuro = new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'});
const content = document.querySelector('main.io section > table');
// const content = document.getElementById('content');
const tbodyPosition = document.getElementById('positions');
tbodyPosition.addEventListener('click', (event) => {
const rows = tbodyPosition.querySelectorAll('tr');
const rowTarget = event.target.closest('tr');
if (rowTarget && event.button === 0) {
for (row of rows) {
row.classList.remove('active');
}
rowTarget.classList.add('active');
}
});
for (let i = 0; i < 256; i++) {
const clone = document.importNode(template.content, true);
const clone = cloneRow();
const tr = clone.querySelector('tr');
const th = clone.querySelector('th');
const td = clone.querySelectorAll('td');
@ -113,13 +143,90 @@ tags:
tr.setAttribute('data-id', i);
th.textContent = i + 1;
td[2].textContent = getRandomFormattedString();
td[3].textContent = placeholderNames[j];
// td[2].innerHTML = replaceLineBreaks(placeholderContents[k]);
td[4].firstElementChild.textContent = placeholderContents[k];
// td[5].textContent = randomIntFrom(1, i).toString();
td[5].textContent = currencyEuro.format(randomFloatFrom(1, 1000000, 2));
tbody.appendChild(clone);
tbodyPosition.appendChild(clone);
}
const selectNum = document.getElementById('sltNum');
const buttonPosNum = document.getElementById('setPosNum');
buttonPosNum.addEventListener('click', () => {
const numType = selectNum.value;
const cells = tbodyPosition.querySelectorAll('td.pos-num');
let num = '';
for (let i = 0; i < cells.length; i++) {
switch (numType) {
case 'numeric':
num = (i + 1).toString();
break;
case 'roman':
num = convertToRomanNumeral((i + 1)).toString();
break;
case '':
default:
}
cells[i].textContent = num;
}
});
const buttonIndex = document.getElementById('tglIndex');
buttonIndex.addEventListener('click', () => {
const cells = content.querySelectorAll('th:first-child');
const isHidden = cells[0].classList.contains('di_none');
for (cell of cells) {
if (isHidden) {
cell.classList.remove('di_none');
} else {
cell.classList.add('di_none');
}
}
});
const buttonAdd = document.getElementById('addEntry');
const checkScroll = document.getElementById('setScroll');
buttonAdd.addEventListener('click', () => {
const clone = cloneRow();
const viewportHeight = window.innerHeight;
const elementActive = tbodyPosition.querySelector('tr.active');
let elementNew = undefined;
let elementBound = undefined;
if (elementActive) {
elementActive.after(clone);
elementNew = elementActive.nextElementSibling;
} else {
tbodyPosition.appendChild(clone);
elementNew = tbodyPosition.lastElementChild;
}
if (checkScroll.checked) {
elementBound = elementNew.getBoundingClientRect();
if (elementBound.bottom > viewportHeight || elementBound.top < 0) {
elementNew.scrollIntoView({behavior: 'smooth', block: 'nearest'});
}
}
// TODO: Neuen index bilden
});
function cloneRow(type = 'default') {
type = 'row' + capitalizeFirstLetter(type);
const rowFragment = document.getElementById(type).content;
// TODO: Datenübergabe ergänzen und direkt hier in die Node aufnehmen
return document.importNode(rowFragment, true);
}
</script>
{% endblock %}

View file

@ -0,0 +1,13 @@
---
title: TUI
tags:
- ui
---
{% assign bodyClass = "body_frame" -%}
{% layout "hippie/app.liquid" %}
{% block body %}
{% render 'hippie/partials/frame-header.liquid' %}
<main class="io"></main>
{% render 'hippie/partials/frame-mode.liquid' %}
{% endblock %}

View file

@ -1,14 +0,0 @@
---
title: TUI
tags:
- ui
---
{% set pageId = page.fileSlug %}
{% extends "hippie/_app_frame.njk" %}
{% block body %}
{{ io.frameHeader('title-bar') }}
<main class="io"></main>
{{ io.frameFooter('mode-bar') }}
{% endblock %}

View file

@ -18,8 +18,8 @@ tags:
<nav class="big">
<button><i class="bi bi-folder"></i></button>
<button><i class="bi bi-terminal"></i></button>
<button id="pauseButton"><i class="bi bi-pause"></i></button>
<button id="resumeButton"><i class="bi bi-play"></i></button>
<button id="setPause"><i class="bi bi-pause"></i></button>
<button id="setPlay"><i class="bi bi-play"></i></button>
</nav>
<div>
<nav class="small">
@ -67,11 +67,11 @@ tags:
const timeFormat = {hour: '2-digit', minute: '2-digit'};
const timeDisplay = new TimeDisplay(timeElement, timeFormat);
document.getElementById('pauseButton').addEventListener('click', () => {
document.getElementById('setPause').addEventListener('click', () => {
timeDisplay.pause();
console.info('Pause time');
});
document.getElementById('resumeButton').addEventListener('click', () => {
document.getElementById('setPlay').addEventListener('click', () => {
timeDisplay.resume();
console.info('Resume time');
});

@ -1 +1 @@
Subproject commit 0623e818a49176ca7516a88943e07a882ba5262d
Subproject commit 530748663f02ac15597c78843a12cd1f9973c4c9