[ux] show horizontal scrollbar in schedule

<template>

          {{ formatDuration(totalManDays) }} {{ $t('schedule.md') }}

            <chevron-right-icon v-if="!rootElement.expanded"/>
            <chevron-down-icon v-else />

            <production-name
              :production="rootElement"
              :only-avatar="true"
              :size="30"
              v-if="rootElement.type === 'Project'"
            />
            <people-avatar
              :person="rootElement"
              :is-link="false"
              :size="30"
              :no-cache="true"
              v-else
            />

            {{ rootElement.name }}

          <router-link
            class="filler flexrow-item root-element-name"
            :to="rootElement.route"
            v-else
          >
            {{ rootElement.name }}
          </router-link>
          <input
            class="flexrow-item mr1"
            type="number"
            step="any"
            placeholder="0"
            @input="$emit('estimation-changed', {
              days: $event.target.value,
              item: rootElement
            })"
            v-if="!rootElement.avatar && rootElement.editable"
            :value="formatDuration(rootElement.man_days)"
          />

            {{ $t('schedule.md') }}

            {{ formatDuration(rootElement.man_days) }}
            {{ $t('schedule.md') }}

            <spinner
              style="width: 20px; margin: 0 0 10px 10px;"
              class="child-spinner flexrow-item"
            />

              <router-link
                :to="childElement.route"
                class="filler flexrow-item child-element-name"
                v-if="childElement.route"
              >
                {{ childElement.name }}
              </router-link>

                {{ childElement.name }}

                <input
                  class="man-days-unit flexrow-item"
                  type="number"
                  min="0"
                  placeholder="0"
                  step="any"
                  @input="onChildEstimationChanged($event, childElement, rootElement)"
                  :value="formatDuration(childElement.man_days)"
                />
                {{ $t('schedule.md') }}

                {{ childElement.man_days }}
                {{ $t('schedule.md') }}

                {{ milestones[day.text].name }}

              •

              +

                {{ day.monthText }}

                 2"
                >
                  {{ day.dayText }} /

                  {{ day.dayNumber }}

                <spinner
                  style="width: 20px; margin: 0 0 10px 10px; opacity: 0"
                  class="child-spinner flexrow-item"
                />

  <edit-milestone-modal
    ref="edit-milestone-modal"
    :active="modals.edit"
    :is-loading="loading.edit"
    :is-error="errors.edit"
    :milestone-to-edit="milestoneToEdit"
    @confirm="confirmEditMilestone"
    @cancel="hideEditMilestoneModal"
    @remove-milestone="removeMilestone"
  />

</template>


updates

updates

<template>

    <q-page padding>

        <q-btn
          flat
          dense
          color="primary"
          size="xs"
          icon="keyboard_arrow_left"
        />
        <q-breadcrumbs>

          <q-breadcrumbs-el
            v-for="locationBreadcrum in clusters.locationBreadcrums"
            :key="locationBreadcrum"
          >
            {{ locationBreadcrum }}
          </q-breadcrumbs-el>
        </q-breadcrumbs>

        {{ item.label }}

            Cluster ID: 0x000{{ item.id }}, Enabled for Server

            Cluster ID: 0x000{{ item.id }}, Disabled for Server

Enabled for Client

Disabled for Client

          <q-toggle
            v-model="clusters.commandDiscovery"
            label="Enable Command Discovery"
          ></q-toggle>
          <q-btn round flat icon="info" size="md" color="grey">
            <q-tooltip
              >An explanation of toggling Enable Command Discovery</q-tooltip
            >
          </q-btn>

        <q-tabs v-model="tab" dense active-color="blue" align="left">
          <q-tab name="attributes" label="Attributes" />
          <q-tab name="reporting" label="Reporting" />
          <q-tab name="commands" label="Commands" />
        </q-tabs>

          <ZclAttributeView />

          <ZclCommandView />

          <ZclReportingView />

    </q-page>

</template>

<template>

    <q-page padding>

        <q-btn
          flat
          dense
          color="primary"
          size="xs"
          icon="keyboard_arrow_left"
        />
        <q-breadcrumbs>

          <q-breadcrumbs-el
            v-for="locationBreadcrum in clusters.locationBreadcrums"
            :key="locationBreadcrum"
          >
            {{ locationBreadcrum }}
          </q-breadcrumbs-el>
        </q-breadcrumbs>

        {{ item.label }}

            Cluster ID: {{ item.id }}, Enabled for Server

Cluster ID: {{ item.id }}, Not Enabled for Server

          <q-toggle
            v-model="clusters.commandDiscovery"
            label="Enable Command Discovery"
          ></q-toggle>
          <q-btn round flat icon="info" size="md" color="grey">
            <q-tooltip
              >An explanation of toggling Enable Command Discovery</q-tooltip
            >
          </q-btn>

        <q-tabs v-model="tab" dense active-color="blue" align="left">
          <q-tab name="attributes" label="Attributes" />
          <q-tab name="reporting" label="Reporting" />
          <q-tab name="commands" label="Commands" />
        </q-tabs>

          <ZclAttributeView />

          <ZclCommandView />

          <ZclReportingView />

    </q-page>

</template>


fix: feedback

fix: feedback

Signed-off-by: Luka Trovic <e69a756ac71279bfe707cf457c3331b5a413c5a7@nextcloud.com>

<template>

			<Actions>
				<ActionButton v-if="showArchived === false && !isCurrentUserAssigned"
					icon="icon-user"
					:close-after-click="true"
					@click="assignCardToMe()">
					{{ t('deck', 'Assign to me') }}
				</ActionButton>
				<ActionButton v-if="showArchived === false && isCurrentUserAssigned"
					icon="icon-user"
					:close-after-click="true"
					@click="unassignCardFromMe()">
					{{ t('deck', 'Unassign myself') }}
				</ActionButton>
				<ActionButton icon="icon-external" :close-after-click="true" @click="modalShow=true">
					{{ t('deck', 'Move card') }}
				</ActionButton>
				<ActionButton icon="icon-settings-dark" :close-after-click="true" @click="openCard">
					{{ t('deck', 'Card details') }}
				</ActionButton>
				<ActionButton icon="icon-archive" :close-after-click="true" @click="archiveUnarchiveCard()">
					{{ card.archived ? t('deck', 'Unarchive card') : t('deck', 'Archive card') }}
				</ActionButton>
				<ActionButton v-if="showArchived === false"
					icon="icon-delete"
					:close-after-click="true"
					@click="deleteCard()">
					{{ t('deck', 'Delete card') }}
				</ActionButton>
			</Actions>

		<Modal v-if="modalShow" :title="t('deck', 'Move card to another board')" @close="modalShow=false">

{{ t('deck', 'Move card to another board') }}

				<Multiselect v-model="selectedBoard"
					:placeholder="t('deck', 'Select a board')"
					:options="activeBoards"
					:max-height="100"
					label="title"
					@select="loadStacksFromBoard" />
				<Multiselect v-model="selectedStack"
					:placeholder="t('deck', 'Select a list')"
					:options="stacksFromBoard"
					:max-height="100"
					label="title">

						{{ t('deck', 'List is empty') }}

				</Multiselect>

				<button :disabled="!isBoardAndStackChoosen" class="primary" @click="moveCard">
					{{ t('deck', 'Move card') }}
				</button>
				<button @click="modalShow=false">
					{{ t('deck', 'Cancel') }}
				</button>

		</Modal>

</template>


Add technical_electives field in SkillInfo form

<template lang="html">

            Expertise Area/Area(s) of Interest

              <textarea class="textarea input" type="text" placeholder="Web Development, App Development, API Development, Open Source" v-model="skill.expertise"></textarea>

            Programming Language(s)

              <textarea class="textarea input" type="text" placeholder="C, C++, Java, JavaScript, PHP, Python" v-model="skill.programming_languages"></textarea>

            Tools and Technologies

              <textarea class="textarea input" type="text" placeholder="Android, Socket.io, Arduino, Firebase" v-model="skill.tools"></textarea>

            Technical Electives

                <textarea class="textarea input" type="text" placeholder="Cryptography, Software Testing" v-model="skill.technical_electives"></textarea>

</template>

<template lang="html">

            Expertise Area/Area(s) of Interest

              <textarea class="textarea input" type="text" placeholder="Web Development, App Development, API Development, Open Source" v-model="skill.expertise"></textarea>

            Programming Language(s)

              <textarea class="textarea input" type="text" placeholder="C, C++, Java, JavaScript, PHP, Python" v-model="skill.programming_languages"></textarea>

            Tools and Technologies

              <textarea class="textarea input" type="text" placeholder="Android, Socket.io, Arduino, Firebase" v-model="skill.tools"></textarea>

</template>


Details Alliance

Details Alliance

<template>

        <svg viewBox="0 0 100 100" class="exchange-icon animated pulse">
          <use xlink:href="#app-icon-arrow"></use>
        </svg>

            <svg viewBox="0 0 100 100" class="foods-icon animated pulse">
              <use xlink:href="#foods-icon-{{ alliance.product.icon }}"></use>
            </svg>

{{ alliance.quantity }}

{{ alliance.users.applicant.name.first }}

{{ alliance.users.giver.name.first }}

{{ alliance.product.name }}

Suivi de vos actions

    <template v-if="alliance.status == 'current'">

      <template v-if="alliance.type == 'applicant'">

        <template v-if="alliance.request.completed && !alliance.availability.completed">

            <fieldset class="step my-side animated">
              <legend>
                <svg viewBox="0 0 100 100" class="alliance-icon">
                  <use xlink:href="#app-icon-helmet"></use>
                </svg>
              </legend>

Ta requête à bien été envoyé

                   à {{ alliance.users.giver.name.first }} le :

{{ allianceCreatedAt }}

            </fieldset>

        </template>

        <template v-if="alliance.request.completed && alliance.availability.completed && !alliance.exchange">

                  <fieldset class="step my-side animated">
                    <legend>
                      <svg viewBox="0 0 100 100" class="alliance-icon">
                        <use xlink:href="#app-icon-helmet"></use>
                      </svg>
                    </legend>

{{ alliance.users.giver.name.first }} est

                        disponible pour le créneau :

Entre {{ alliance.availability.timeStart }} et {{ alliance.availability.timeEnd }}

                      le {{ alliance.availability.date }}
                  </fieldset>

                  <fieldset class="step my-side animated">
                    <legend>
                      <svg viewBox="0 0 100 100" class="alliance-icon">
                        <use xlink:href="#app-icon-helmet"></use>
                      </svg>
                    </legend>

Tu es un sacré veinard ! {{ alliance.users.giver.name.first }}

                      est disponible pour le créneau:

Entre {{ alliance.request.timeStart }} et {{ alliance.request.timeEnd }}

                      {{ alliance.request.date }}
                  </fieldset>

                  Tu le trouveras à l'adresse suivante:

                  {{ alliance.users.giver.name.first }} {{ alliance.users.giver.name.last }}

                  {{ foodkeeper.location.street }}

                  {{ foodkeeper.location.additional }}<v-if="foodkeeper.location.additional " br/>
                  {{ foodkeeper.location.zipcode }} {{ foodkeeper.location.city }}

                  Voici des informations complèmentaires:
                  {{ foodkeeper.location.infos }}

              <fieldset class="step user-side animated">
                <legend>
                  <svg viewBox="0 0 100 100" class="alliance-icon">
                    <use xlink:href="#app-icon-bugle"></use>
                  </svg>
                </legend>
                <form>

Fichtre,

                  {{ alliance.users.giver.name.first }} n'est pas disponible
                    <template v-if="alliance.request.delayed">
 pour le créneau horaire que tu lui a indiqué</template>
                    <template v-else>maintenant</template>

Mais il te propose l'horaire suivant:

                    <input type="radio" id="date-1" class="filter" value="false" v-model="productType">

Entre {{ alliance.availability.timeStart }} et
                      {{ alliance.availability.timeEnd }}

                      le {{ alliance.availability.date }}

                </form>
              </fieldset>
              <fieldset class="step final-step animated">
                <form>
                  <svg viewBox="0 0 100 100" class="alliance-icon">
                    <use xlink:href="#app-icon-note"></use>
                  </svg>

Ce créneau te convient-il ?

Oui

Non

                </form>
              </fieldset>

        </template>

        <template v-if="alliance.request.completed && alliance.availability.completed && alliance.exchange">

            <fieldset class="step final-step animated">
                <svg viewBox="0 0 100 100" class="alliance-icon">
                  <use xlink:href="#app-icon-note"></use>
                </svg>

                  Qu'as tu pensé de cette alliance ?

                  Elle était :
                <select class="market-add-form-select" v-model="form.review">
                  <option value="5">Parfait</option>
                  <option value="4">Très Bien</option>
                  <option value="3" selected>Bien</option>
                  <option value="2">Passable</option>
                  <option value="1">A éviter</option>
                  <option value="0">A bannir</option>
                </select>

Valider

            </fieldset>

        </template>

      </template>

      <template v-if="alliance.type == 'giver'">

        <template v-if="alliance.request.completed && !alliance.availability.completed">

            <fieldset class="step user-side animated">
              <legend>
                <svg viewBox="0 0 100 100" class="alliance-icon">
                  <use xlink:href="#app-icon-bugle"></use>
                </svg>
              </legend>
              <form>

Tu viens de recevoir une requête

                  {{ alliance.users.applicant.name.first }} demande ta denrée située

                  dans ton garde-manger

{{ foodkeeper.name }}

                  Pour la récupération de ta denrée,

                  {{ alliance.users.applicant.name.first }}
                  te propose le créneau suivant:

                    <input type="radio" id="date-1" class="filter" value="false" v-model="productType">

Entre {{ alliance.request.timeStart }} et {{ alliance.request.timeEnd }}

                      le {{ alliance.request.date }}

Es-tu disponible dès maintenant ?

Oui

Non

              </form>
            </fieldset>

        </template>

        <template v-if="alliance.request.completed && alliance.availability.completed && !alliance.exchange">
          <template v-if="alliance.availability.applicant || alliance.availability.giver">

              <fieldset class="step user-side animated">
                <legend>
                  <svg viewBox="0 0 100 100" class="alliance-icon">
                    <use xlink:href="#app-icon-bugle"></use>
                  </svg>
                </legend>
                <form>
                  As-tu donné la denrée desirée ?

Oui

Non

                </form>
              </fieldset>

          </template>
          <template v-else>

              <fieldset class="step final-step animated">
                <form>
                  <svg viewBox="0 0 100 100" class="alliance-icon">
                    <use xlink:href="#app-icon-note"></use>
                  </svg>

En attente de la confirmation de la disponibilité du demandeur...

                </form>
              </fieldset>

          </template>
        </template>

        <template v-if="alliance.request.completed && alliance.availability.completed && alliance.exchange">

            <fieldset class="step final-step animated">
              <form>
                <svg viewBox="0 0 100 100" class="alliance-icon">
                  <use xlink:href="#app-icon-note"></use>
                </svg>

                Ton allié ne t'a toujours pas évalué
 pour cet échange
              </form>
            </fieldset>

        </template>
      </template>
    </template>

    <template v-if="alliance.status == 'terminated'">

        <template v-if="alliance.type == 'applicant'">
          <fieldset class="step final-step animated">
            <form>
              <svg viewBox="0 0 100 100" class="alliance-icon">
                <use xlink:href="#app-icon-note"></use>
              </svg>

Hip Hip Hip Houra !

              Une étape de plus a été franchie dans la quête du Pain Perdu !
            </form>
          </fieldset>
        </template>
        <template v-else>
          <fieldset class="step final-step animated">
            <form>
              <svg viewBox="0 0 100 100" class="alliance-icon">
                <use xlink:href="#app-icon-note"></use>
              </svg>

Hip Hip Hip Houra !

              Une étape de plus a été franchie

              dans la quête du Pain Perdu !

            </form>

Voir l'avis obtenu

          </fieldset>
        </template>

    </template>

    <template v-if="alliance.status == 'abandoned'">

        <fieldset class="step final-step animated">
          <form>
            <svg viewBox="0 0 100 100" class="alliance-icon">
              <use xlink:href="#app-icon-note"></use>
            </svg>

Diantre,

            Cette alliance s'avère compliquée !

            Réessaye plus tard lorsque
 les conditions seront plus favorables !
          </form>
        </fieldset>

    </template>

    <slot-popup
      :alliance-id="$route.params.id"
      :update-alliance="updateCurrentAlliance">
    </slot-popup>
    <confirmation-popup
      :user-selection="userSelection"
      :update-alliance="updateCurrentAlliance">
    </confirmation-popup>

</template>

<template>

        <svg viewBox="0 0 100 100" class="exchange-icon animated pulse">
          <use xlink:href="#app-icon-arrow"></use>
        </svg>

            <svg viewBox="0 0 100 100" class="foods-icon animated pulse">
              <use xlink:href="#foods-icon-{{ alliance.product.icon }}"></use>
            </svg>

{{ alliance.quantity }}

{{ alliance.users.applicant.name.first }}

{{ alliance.users.giver.name.first }}

{{ alliance.product.name }}

Suivi de vos actions

    <template v-if="alliance.status == 'current'">

      <template v-if="alliance.type == 'applicant'">

        <template v-if="alliance.request.completed && !alliance.availability.completed">

            <fieldset class="step my-side animated">
              <legend>
                <svg viewBox="0 0 100 100" class="alliance-icon">
                  <use xlink:href="#app-icon-helmet"></use>
                </svg>
              </legend>

Ta requête à bien été envoyé

                   à {{ alliance.users.giver.name.first }} le :

{{ allianceCreatedAt }}

            </fieldset>

        </template>

        <template v-if="alliance.request.completed && alliance.availability.completed && !alliance.exchange">

                  <fieldset class="step my-side animated">
                    <legend>
                      <svg viewBox="0 0 100 100" class="alliance-icon">
                        <use xlink:href="#app-icon-helmet"></use>
                      </svg>
                    </legend>

{{ alliance.users.giver.name.first }} est

                        disponible pour le créneau :

Entre {{ alliance.availability.timeStart }} et {{ alliance.availability.timeEnd }}

                      le {{ alliance.availability.date }}
                  </fieldset>

                  <fieldset class="step my-side animated">
                    <legend>
                      <svg viewBox="0 0 100 100" class="alliance-icon">
                        <use xlink:href="#app-icon-helmet"></use>
                      </svg>
                    </legend>

Tu es un sacré vénard ! {{ alliance.users.giver.name.first }}

                      est disponible pour le créneau:

Entre {{ alliance.request.timeStart }} et {{ alliance.request.timeEnd }}

                      {{ alliance.request.date }}
                  </fieldset>

                  Tu le trouveras à l'adresse suivante:

                  {{ alliance.users.giver.name.first }} {{ alliance.users.giver.name.last }}

                  {{ foodkeeper.location.street }}

                  {{ foodkeeper.location.additional }}<v-if="foodkeeper.location.additional " br/>
                  {{ foodkeeper.location.zipcode }} {{ foodkeeper.location.city }}

                  Voici des informations complèmentaires:
                  {{ foodkeeper.location.infos }}

              <fieldset class="step user-side animated">
                <legend>
                  <svg viewBox="0 0 100 100" class="alliance-icon">
                    <use xlink:href="#app-icon-bugle"></use>
                  </svg>
                </legend>
                <form>

Fichtre,

                  {{ alliance.users.giver.name.first }} n'est pas disponible
                    <template v-if="alliance.request.delayed">
 pour le créneau horaire que tu lui a indiqué</template>
                    <template v-else>maintenant</template>

Mais il te propose l'horaire suivant:

                    <input type="radio" id="date-1" class="filter" value="false" v-model="productType">

Entre {{ alliance.availability.timeStart }} et
                      {{ alliance.availability.timeEnd }}

                      le {{ alliance.availability.date }}

                </form>
              </fieldset>
              <fieldset class="step final-step animated">
                <form>
                  <svg viewBox="0 0 100 100" class="alliance-icon">
                    <use xlink:href="#app-icon-note"></use>
                  </svg>

Ce créneau te convient-il ?

Oui

Non

                </form>
              </fieldset>

        </template>

        <template v-if="alliance.request.completed && alliance.availability.completed && alliance.exchange">

            <fieldset class="step final-step animated">
                <svg viewBox="0 0 100 100" class="alliance-icon">
                  <use xlink:href="#app-icon-note"></use>
                </svg>

                  Qu'as tu pensé de cette alliance ?

                  Elle était :
                <select class="market-add-form-select" v-model="form.review">
                  <option value="5">Parfait</option>
                  <option value="4">Très Bien</option>
                  <option value="3" selected>Bien</option>
                  <option value="2">Passable</option>
                  <option value="1">A éviter</option>
                  <option value="0">A bannir</option>
                </select>

Valider

            </fieldset>

        </template>

      </template>

      <template v-if="alliance.type == 'giver'">

        <template v-if="alliance.request.completed && !alliance.availability.completed">

            <fieldset class="step user-side animated">
              <legend>
                <svg viewBox="0 0 100 100" class="alliance-icon">
                  <use xlink:href="#app-icon-bugle"></use>
                </svg>
              </legend>
              <form>

Tu viens de recevoir une requête

                  {{ alliance.users.applicant.name.first }} demande ta denrée située

                  dans ton garde-manger

{{ foodkeeper.name }}

                  Pour la récupération de ta denrée,

                  {{ alliance.users.applicant.name.first }}
                  te propose le créneau suivant:

                    <input type="radio" id="date-1" class="filter" value="false" v-model="productType">

Entre {{ alliance.request.timeStart }} et {{ alliance.request.timeEnd }}

                      le {{ alliance.request.date }}

Es-tu disponible dès maintenant ?

Oui

Non

              </form>
            </fieldset>

        </template>

        <template v-if="alliance.request.completed && alliance.availability.completed && !alliance.exchange">
          <template v-if="alliance.availability.applicant || alliance.availability.giver">

              <fieldset class="step user-side animated">
                <legend>
                  <svg viewBox="0 0 100 100" class="alliance-icon">
                    <use xlink:href="#app-icon-bugle"></use>
                  </svg>
                </legend>
                <form>
                  As-tu donné la denrée desirée ?

Oui

Non

                </form>
              </fieldset>

          </template>
          <template v-else>

              <fieldset class="step final-step animated">
                <form>
                  <svg viewBox="0 0 100 100" class="alliance-icon">
                    <use xlink:href="#app-icon-note"></use>
                  </svg>

En attente de la confirmation de la disponibilité du demandeur...

                </form>
              </fieldset>

          </template>
        </template>

        <template v-if="alliance.request.completed && alliance.availability.completed && alliance.exchange">

            <fieldset class="step final-step animated">
              <form>
                <svg viewBox="0 0 100 100" class="alliance-icon">
                  <use xlink:href="#app-icon-note"></use>
                </svg>

                Ton allié ne t'a toujours pas évalué
 pour cet échange
              </form>
            </fieldset>

        </template>
      </template>
    </template>

    <template v-if="alliance.status == 'terminated'">

        <template v-if="alliance.type == 'applicant'">
          <fieldset class="step final-step animated">
            <form>
              <svg viewBox="0 0 100 100" class="alliance-icon">
                <use xlink:href="#app-icon-note"></use>
              </svg>

Hip Hip Hip Houra !

              Une étape de plus a été franchie dans la quête du Pain Perdu !
            </form>
          </fieldset>
        </template>
        <template v-else>
          <fieldset class="step final-step animated">
            <form>
              <svg viewBox="0 0 100 100" class="alliance-icon">
                <use xlink:href="#app-icon-note"></use>
              </svg>

Hip Hip Hip Houra !

              Une étape de plus a été franchie

              dans la quête du Pain Perdu !

            </form>

Voir l'avis obtenu

          </fieldset>
        </template>

    </template>

    <template v-if="alliance.status == 'abandoned'">

        <fieldset class="step final-step animated">
          <form>
            <svg viewBox="0 0 100 100" class="alliance-icon">
              <use xlink:href="#app-icon-note"></use>
            </svg>

Diantre,

            Cette alliance s'avère compliquée !

            Réessaye plus tard lorsque
 les conditions seront plus favorables !
          </form>
        </fieldset>

    </template>

    <slot-popup
      :alliance-id="$route.params.id"
      :update-alliance="updateCurrentAlliance">
    </slot-popup>
    <confirmation-popup
      :user-selection="userSelection"
      :update-alliance="updateCurrentAlliance">
    </confirmation-popup>

</template>


Added title

Added title

<template>

{{ result.title }}

      {{ pron.phsym }} <speaker v-if="pron.audio" :src="pron.audio"></speaker>

= 0 || result.level">
      <star-rates :rate="result.star" :width="15" :gutter="4"></star-rates>
      {{ result.level }}

</template>

<template>

= 0 || result.level">
      <star-rates :rate="result.star" :width="15" :gutter="4"></star-rates>
      {{ result.level }}

      {{ pron.phsym }} <speaker v-if="pron.audio" :src="pron.audio"></speaker>

</template>


Comp multiplier select metrics adjustment

<template>
    <Wrapper v-if="prefsLoaded">
        <Dropzone
            :accepts="[ '.json' ]"
            auto-read auto-parse @read="buildLayers"
        />
        <Row margin="0 auto 8px auto" style="width: 168px">
            <Button-Group
                :active="(prefs.newComp) ? 0 : 1"
                exclusive
                @update="val => setPref('newComp', val == 0)">
                <Button
                    prefix-icon="plus"
                    label="New Comp"
                    tooltip="Add layers to new comp"
                    tall
                    margin="0px" />
                <Button
                    prefix-icon="arrow-down"
                    label="Current"
                    tooltip="Add layers to open comp"
                    tall
                    margin="0px" />
            </Button-Group>
        </Row>

        <Fold
			label="Options"
			:open="true"
            prefs-id="foldOptions">

Image save path:

            <Folder-Picker
                :style="{ 'margin-left':'-4px !important', 'margin-bottom': '6px' }"
                folder
                prefs-id="folderPath"
                @prefs-update="updatePrefs"
                ref="folder"
                label="Filepath"
                :prefs="prefs"
                full-path
                clearable
                no-label
            />

            <Select
                v-show="prefs.newComp"
                :items="compScaleOptions"
                :active="prefs.compScale - 1"
                label="Comp size multiplier"
                label-to-right
                @update="val => setPref('compScale', parseInt(compScaleOptions[val].value) + 1)"
                no-indicator
            />
            <Input-Scroll
                v-show="prefs.newComp"
                label='Frame rate:'
                lazy
                suffix="fps"
                :size="10"
                @change="val => setPref('frameRate', val)"
                :value="prefs.frameRate"
                :reset-value="prefs.frameRate"
                :min="1"
                :max="99" />
            <Input-Scroll
                v-show="prefs.newComp"
                label='Comp duration:'
                lazy
                suffix="seconds"
                :size="10"
                @change="val => setPref('duration', val)"
                :value="prefs.duration"
                :reset-value="prefs.duration"
                :min="1"
                :max="1000" />
			<Toggle
				label="Detect parametric shapes"
				:state="prefs.parametrics"
				@update="val => setPref('parametrics', val)"
			/>
			<Toggle
				label="Precomp groups"
				:state="prefs.precompGroups"
				@update="val => setPref('precompGroups', val)"
			/>
		</Fold>

        <Fold label="Groups"
            :open="false"
            prefs-id="foldGroups">
			<Button-Group grid column>
				<Button
					left
					tall
					icon-size="16px"
					prefix-icon="image-filter-center-focus-weak"
                    tooltip="Group selected layers"
					label="Precomp"
                    @click="aeCall('groupToPrecomp')"
				/>
				<Button
					left
					tall
					icon-size="16px"
					prefix-icon="arrow-expand-all"
                    tooltip="Expand layers from selected precomps"
					label="Un-Precomp"
                    @click="aeCall('precompToLayers')"
				/>
				<Button
					left
					tall
					icon-size="16px"
					prefix-icon="eye-off"
                    tooltip="Show/hide all guide layers"
					label="Toggle guide layer visibility"
                    @click="aeCall('toggleGroupVisibility')"
				/>
				<Button
					left
					tall
					icon-size="16px"
					prefix-icon="delete"
                    tooltip="Remove all AEUX created guide layers"
					label="Delete group layers"
                    @click="aeCall('deleteGroupLayers')"
				/>
			</Button-Group>
		</Fold>

        <Fold label="System"
            :open="false"
            prefs-id="foldSystem">
            <Button
                block
                goto="https://aeux.io"
                tooltip="aeux.io"
                @click.alt.native="openConfig">
                Learn stuff
            </Button>
            <Panel-Info name uppercase version>
                Brought to you by your friends at Google motion design
            </Panel-Info>
        </Fold>

    </Wrapper>
</template>

<template>
    <Wrapper v-if="prefsLoaded">
        <Dropzone
            :accepts="[ '.json' ]"
            auto-read auto-parse @read="buildLayers"
        />
        <Row margin="0 auto 8px auto" style="width: 168px">
            <Button-Group
                :active="(prefs.newComp) ? 0 : 1"
                exclusive
                @update="val => setPref('newComp', val == 0)">
                <Button
                    prefix-icon="plus"
                    label="New Comp"
                    tooltip="Add layers to new comp"
                    tall
                    margin="0px" />
                <Button
                    prefix-icon="arrow-down"
                    label="Current"
                    tooltip="Add layers to open comp"
                    tall
                    margin="0px" />
            </Button-Group>
        </Row>

        <Fold
			label="Options"
			:open="true"
            prefs-id="foldOptions">

Image save path:

            <Folder-Picker
                :style="{ 'margin-left':'-4px !important', 'margin-bottom': '6px' }"
                folder
                prefs-id="folderPath"
                @prefs-update="updatePrefs"
                ref="folder"
                label="Filepath"
                :prefs="prefs"
                full-path
                clearable
                no-label
            />

            <Select
                v-show="prefs.newComp"
                :items="compScaleOptions"
                :active="prefs.compScale - 1"
                label="Comp size multiplier"
                label-to-right
                @update="val => setPref('compScale', parseInt(compScaleOptions[val].value) + 1)"
            />
            <Input-Scroll
                v-show="prefs.newComp"
                label='Frame rate:'
                lazy
                suffix="fps"
                :size="10"
                @change="val => setPref('frameRate', val)"
                :value="prefs.frameRate"
                :reset-value="prefs.frameRate"
                :min="1"
                :max="99" />
            <Input-Scroll
                v-show="prefs.newComp"
                label='Comp duration:'
                lazy
                suffix="seconds"
                :size="10"
                @change="val => setPref('duration', val)"
                :value="prefs.duration"
                :reset-value="prefs.duration"
                :min="1"
                :max="1000" />
			<Toggle
				label="Detect parametric shapes"
				:state="prefs.parametrics"
				@update="val => setPref('parametrics', val)"
			/>
			<Toggle
				label="Precomp groups"
				:state="prefs.precompGroups"
				@update="val => setPref('precompGroups', val)"
			/>
		</Fold>

        <Fold label="Groups"
            :open="false"
            prefs-id="foldGroups">
			<Button-Group grid column>
				<Button
					left
					tall
					icon-size="16px"
					prefix-icon="image-filter-center-focus-weak"
                    tooltip="Group selected layers"
					label="Precomp"
                    @click="aeCall('groupToPrecomp')"
				/>
				<Button
					left
					tall
					icon-size="16px"
					prefix-icon="arrow-expand-all"
                    tooltip="Expand layers from selected precomps"
					label="Un-Precomp"
                    @click="aeCall('precompToLayers')"
				/>
				<Button
					left
					tall
					icon-size="16px"
					prefix-icon="eye-off"
                    tooltip="Show/hide all guide layers"
					label="Toggle guide layer visibility"
                    @click="aeCall('toggleGroupVisibility')"
				/>
				<Button
					left
					tall
					icon-size="16px"
					prefix-icon="delete"
                    tooltip="Remove all AEUX created guide layers"
					label="Delete group layers"
                    @click="aeCall('deleteGroupLayers')"
				/>
			</Button-Group>
		</Fold>

        <Fold label="System"
            :open="false"
            prefs-id="foldSystem">
            <Button
                block
                goto="https://aeux.io"
                tooltip="aeux.io"
                @click.alt.native="openConfig">
                Learn stuff
            </Button>
            <Panel-Info name uppercase version>
                Brought to you by your friends at Google motion design
            </Panel-Info>
        </Fold>

    </Wrapper>
</template>


Update calendar.vue

Update calendar.vue

<template>

	<mk-widget-container :naked="props.design == 1" :show-header="false">

					{{ year }}年
					{{ month }}月

{{ day }}日

{{ weekDay }}曜日

今日:{{ dayP.toFixed(1) }}%

						<div class="val" :style="{ width: `${dayP}%` }">

今月:{{ monthP.toFixed(1) }}%

						<div class="val" :style="{ width: `${monthP}%` }">

今年:{{ yearP.toFixed(1) }}%

						<div class="val" :style="{ width: `${yearP}%` }">

	</mk-widget-container>

</template>


Update Footer.vue

Update Footer.vue


:art:

:art:

<template>
<div class="vswabwbm" :style="{ top: `${y - 64}px`, left: `${x - 64}px` }" :class="{ active }">
	<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
		<circle fill="none" cx="64" cy="64">
			<animate attributeName="r"
					begin="0s" dur="0.5s"
					values="4; 32"
					calcMode="spline"
					keyTimes="0; 1"
					keySplines="0.165, 0.84, 0.44, 1"
					repeatCount="1" />
			<animate attributeName="stroke-width"
					begin="0s" dur="0.5s"
					values="16; 0"
					calcMode="spline"
					keyTimes="0; 1"
					keySplines="0.3, 0.61, 0.355, 1"
					repeatCount="1" />
		</circle>
		<g fill="none" fill-rule="evenodd">
			<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color">
				<animate attributeName="r"
						begin="0s" dur="0.8s"
						:values="`${particle.size}; 0`"
						calcMode="spline"
						keyTimes="0; 1"
						keySplines="0.165, 0.84, 0.44, 1"
						repeatCount="1" />
				<animate attributeName="cx"
						begin="0s" dur="0.8s"
						:values="`${particle.xA}; ${particle.xB}`"
						calcMode="spline"
						keyTimes="0; 1"
						keySplines="0.3, 0.61, 0.355, 1"
						repeatCount="1" />
				<animate attributeName="cy"
						begin="0s" dur="0.8s"
						:values="`${particle.yA}; ${particle.yB}`"
						calcMode="spline"
						keyTimes="0; 1"
						keySplines="0.3, 0.61, 0.355, 1"
						repeatCount="1" />
			</circle>
		</g>
	</svg>
</div>
</template>

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
	props: {
		x: {
			type: Number,
			required: true
		},
		y: {
			type: Number,
			required: true
		}
	},
	data() {
		const particles = [];
		const origin = 64;
		const colors = ['#FF1493', '#00FFFF', '#FFE202'];

		for (let i = 0; i < 12; i++) {
			const angle = Math.random() * (Math.PI * 2);
			const pos = Math.random() * 16;
			const velocity = 16 + (Math.random() * 48);
			particles.push({
				size: 4 + (Math.random() * 8),
				xA: origin + (Math.sin(angle) * pos),
				yA: origin + (Math.cos(angle) * pos),
				xB: origin + (Math.sin(angle) * (pos + velocity)),
				yB: origin + (Math.cos(angle) * (pos + velocity)),
				color: colors[Math.floor(Math.random() * colors.length)]
			});
		}

		return {
			particles
		};
	},
	mounted() {
		setTimeout(() => {
			this.destroyDom();
		}, 1100);
	}
});
</script>

<style lang="scss" scoped>
.vswabwbm {
	pointer-events: none;
	position: fixed;
	z-index: 1000000;
	width: 128px;
	height: 128px;

	> svg {
		> circle {
			stroke: var(--accent);
		}
	}
}
</style>

<template>
<div class="vswabwbm" :style="{ top: `${y - 64}px`, left: `${x - 64}px` }" :class="{ active }">
	<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
		<circle fill="none" cx="64" cy="64">
			<animate attributeName="r"
					begin="0s" dur="1s"
					values="4; 32"
					calcMode="spline"
					keyTimes="0; 1"
					keySplines="0.165, 0.84, 0.44, 1"
					repeatCount="1" />
			<animate attributeName="stroke-width"
					begin="0s" dur="1s"
					values="16; 0"
					calcMode="spline"
					keyTimes="0; 1"
					keySplines="0.3, 0.61, 0.355, 1"
					repeatCount="1" />
		</circle>
		<g fill="none" fill-rule="evenodd">
			<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color">
				<animate attributeName="r"
						begin="0s" dur="1s"
						:values="`${particle.size}; 0`"
						calcMode="spline"
						keyTimes="0; 1"
						keySplines="0.165, 0.84, 0.44, 1"
						repeatCount="1" />
				<animate attributeName="cx"
						begin="0s" dur="1s"
						:values="`${particle.xA}; ${particle.xB}`"
						calcMode="spline"
						keyTimes="0; 1"
						keySplines="0.3, 0.61, 0.355, 1"
						repeatCount="1" />
				<animate attributeName="cy"
						begin="0s" dur="1s"
						:values="`${particle.yA}; ${particle.yB}`"
						calcMode="spline"
						keyTimes="0; 1"
						keySplines="0.3, 0.61, 0.355, 1"
						repeatCount="1" />
			</circle>
		</g>
	</svg>
</div>
</template>

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
	props: {
		x: {
			type: Number,
			required: true
		},
		y: {
			type: Number,
			required: true
		}
	},
	data() {
		const particles = [];
		const origin = 64;
		const colors = ['#FF1493', '#00FFFF', '#FFE202'];

		for (let i = 0; i < 12; i++) {
			const angle = Math.random() * (Math.PI * 2);
			const pos = Math.random() * 16;
			const velocity = 16 + (Math.random() * 48);
			particles.push({
				size: 4 + (Math.random() * 8),
				xA: origin + (Math.sin(angle) * pos),
				yA: origin + (Math.cos(angle) * pos),
				xB: origin + (Math.sin(angle) * (pos + velocity)),
				yB: origin + (Math.cos(angle) * (pos + velocity)),
				color: colors[Math.floor(Math.random() * colors.length)]
			});
		}

		return {
			particles
		};
	},
	mounted() {
		setTimeout(() => {
			this.destroyDom();
		}, 1100);
	}
});
</script>

<style lang="scss" scoped>
.vswabwbm {
	pointer-events: none;
	position: fixed;
	z-index: 1000000;
	width: 128px;
	height: 128px;

	> svg {
		> circle {
			stroke: var(--accent);
		}
	}
}
</style>


Fix icon search not being case-insensitive when searching Array properties

<template>
  <div class="icon-list">
    <button
      v-for="icon in visibleIcons"
      :key="icon.id"
      :class="{'is-primary': icon.id === value.id, 'is-transparent': icon.id !== value.id}"
      :title="icon.name"
      type="button"
      class="button"
      @click="select(icon)">
      <b-icon
        :pack="iconPack.id"
        :icon="icon.id"/>
    </button>
  </div>
</template>

<script>
import { selectedIcon } from './props';

export default {
  props: {
    iconPack: {
      type: Object,
      default: null,
      validator(value) {
        return Array.isArray(value.icons)
            && typeof value.id === 'string'
            && typeof value.name === 'string';
      },
    },
    search: {
      type: String,
      default: '',
    },
    value: selectedIcon,
  },
  computed: {
    searchLowerCase() {
      return this.search.toLowerCase();
    },
    visibleIcons() {
      const { icons } = this.iconPack;
      if (this.search && this.search.length > 0) {
        return icons.filter((icon) => {
          const toMatch = ['id', 'name', 'filter', 'categories'];
          for (const prop of toMatch) {
            if (icon[prop]) {
              if (typeof icon[prop] === 'string' && icon[prop].toLowerCase().includes(this.searchLowerCase)) {
                return true;
              } else if (Array.isArray(icon[prop])) {
                const regex = new RegExp(this.searchLowerCase, 'i');
                const match = regex.test(icon[prop].join('|'));
                if (match) {
                  return true;
                }
              }
            }
          }
          return false;
        });
      }
      return icons;
    },
  },
  methods: {
    select(icon) {
      this.$emit('select', {
        id: icon.id,
        name: icon.name,
        pack: this.iconPack.id,
      });
    },
  },
};
</script>

<template>
  <div class="icon-list">
    <button
      v-for="icon in visibleIcons"
      :key="icon.id"
      :class="{'is-primary': icon.id === value.id, 'is-transparent': icon.id !== value.id}"
      :title="icon.name"
      type="button"
      class="button"
      @click="select(icon)">
      <b-icon
        :pack="iconPack.id"
        :icon="icon.id"/>
    </button>
  </div>
</template>

<script>
import { selectedIcon } from './props';

export default {
  props: {
    iconPack: {
      type: Object,
      default: null,
      validator(value) {
        return Array.isArray(value.icons)
            && typeof value.id === 'string'
            && typeof value.name === 'string';
      },
    },
    search: {
      type: String,
      default: '',
    },
    value: selectedIcon,
  },
  computed: {
    searchLowerCase() {
      return this.search.toLowerCase();
    },
    visibleIcons() {
      const { icons } = this.iconPack;
      if (this.search && this.search.length > 0) {
        return icons.filter((icon) => {
          const toMatch = ['id', 'name', 'filter', 'categories'];
          for (const prop of toMatch) {
            if (icon[prop]) {
              if (typeof icon[prop] === 'string' && icon[prop].toLowerCase().includes(this.searchLowerCase)) {
                return true;
              } else if (Array.isArray(icon[prop])) {
                const regex = new RegExp(this.searchLowerCase);
                const match = regex.test(icon[prop].join('|'));
                if (match) {
                  return true;
                }
              }
            }
          }
          return false;
        });
      }
      return icons;
    },
  },
  methods: {
    select(icon) {
      this.$emit('select', {
        id: icon.id,
        name: icon.name,
        pack: this.iconPack.id,
      });
    },
  },
};
</script>


Fix filter builder

Fix filter builder

* There was a filter duplication when setting an assignation
* Descriptor with choices didn't have the right default value


fix get data

fix get data

<template>

    <form :model="company" id="company">

      <vue-input v-model="company.name" label placeholder="Наименование организации" icon="building"/>

      <vue-select :list="scopes" :selected-item="company.scope" label="Сфера деятельности" @select="onSelect" icon="tag"/>

      <vue-input v-model="company.address" label placeholder="Адрес" icon="address-card"/>

            Электронный адрес
            <template v-for="(email, index) in company.emails">
              <vue-input v-model="company.emails[index].email" type="email" placeholder="Электронный адрес" icon="envelope" autocomplete="email" @blur="onBlur('emails', 'email')"/>
            </template>

            Телефон
            <template v-for="(phone, index) in company.phones">
              <vue-input v-model="company.phones[index].phone" type="tel" placeholder="Телефон" icon="phone" autocomplete="tel" @blur="onBlur('phones', 'phone')"/>
            </template>

            Факс
            <template v-for="(fax, index) in company.faxes">
              <vue-input v-model="company.faxes[index].phone" type="tel" placeholder="Факс" icon="phone" autocomplete="tel" @blur="onBlur('faxes', 'phone')"/>
            </template>

        Тренировки
        <template v-for="practice in company.practices">
          <vue-input :value="practice.date_str + ' - ' + practice.kind_name + ' - ' + practice.topic" :hyper="'/practice/' + practice.id" icon="history" disabled/>
        </template>

        Сотрудники
        <template v-for="contact in company.contacts">
          <vue-input :value="contact.name + ' - ' + contact.post_name" :hyper="'/contact/' + contact.id" icon="user" disabled/>
        </template>

      <vue-input v-model="company.note" label placeholder="Заметка" icon="sticky-note"/>

            <vue-button text="Сохранить" color="primary" @click="submit"/>

            <vue-button text="Закрыть" @click="close"/>

            <vue-button text="Удалить" color="danger" onclick="return confirm('Вы действительно хотите удалить эту запись?');"/>

    </form>

</template>


Comment out invalid scss

Comment out invalid scss

<template>

  <ui-toolbar
    :title="title"
    type="colored"
    textColor="white"
    class="app-bar"
    :style="{ height: height + 'px' }"
  >
    <ui-icon-button
      slot="icon"
      type="secondary"
      @click="$emit('toggleSideNav')"
    >
      <mat-svg
        class="icon"
        name="menu"
        category="navigation"
      />
    </ui-icon-button>

      {{ title }}

      <slot name="app-bar-actions"></slot>

      <ui-button
        ref="userMenuButton"
        type="primary"
        color="primary"
        class="user-menu-button"
        :ariaLabel="$tr('userMenu')"
        @click="userMenuDropdownIsOpen = !userMenuDropdownIsOpen"
      >
        <mat-svg
          slot="icon"
          name="person"
          category="social"
        />
        <template v-if="isUserLoggedIn">{{ username }}</template>
        <mat-svg name="arrow_drop_down" category="navigation" />
      </ui-button>

      <core-menu
        v-show="userMenuDropdownIsOpen"
        ref="userMenuDropdown"
        class="user-menu-dropdown"
        :raised="true"
        :containFocus="true"
        :hasIcons="true"
        @close="userMenuDropdownIsOpen = false"
      >
        <template slot="header" v-if="isUserLoggedIn">

{{ $tr('role') }}

{{ $tr('admin') }}

{{ $tr('coach') }}

{{ $tr('learner') }}

        </template>

        <template slot="options">
          <component v-for="component in menuOptions" :is="component" :key="component.name" />
          <core-menu-option
            :label="$tr('languageSwitchMenuOption')"
            @select="showLanguageModal = true"
          >
            <mat-svg
              slot="icon"
              name="language"
              category="action"
            />
          </core-menu-option>
          <logout v-if="isUserLoggedIn" />
        </template>

      </core-menu>

      <language-switcher-modal
        v-if="showLanguageModal"
        @close="showLanguageModal = false"
        class="override-ui-toolbar"
      />

  </ui-toolbar>

</template>


formatting changes

formatting changes

<template lang="html">

                zoom_out_map

{{ sharedState.metric.config.title }}, {{ sharedState.year }}

 0">
                    SELECTED
                    {{ privateState.selected }}

                    COUNTY
                    {{ privateState.area }}

                 ({{ sharedState.metric.config.label.toLowerCase() }}).

Total :

                 0" style="white-space: nowrap;">
                    Selected: {{privateState.selectedRaw}} •

                    County: {{privateState.areaRaw}}

                <svg  v-if="sharedState.breaks" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet" viewBox="0 0 248.4 39.2"id="maplegend" role="img" aria-labelledby="svgTitle">
                    <title id="svgTitle">Choropleth legend</title>
                    <g transform="translate(20.714293 -851.75475)">
                        <rect y="865.9" x="-20.7" height="25" width="50" v-bind:style="{fill: this.sharedState.colors[0]}"/>
                        <rect width="50" height="25" x="28.9" y="865.9" v-bind:style="{fill: this.sharedState.colors[1]}"/>
                        <rect width="50" height="25" x="78.5" y="865.9" v-bind:style="{fill: this.sharedState.colors[2]}"/>
                        <rect y="865.9" x="128.1" height="25" width="50" v-bind:style="{fill: this.sharedState.colors[3]}"/>
                        <rect width="50" height="25" x="177.6" y="865.9" v-bind:style="{fill: this.sharedState.colors[4]}"/>
                        <text x="-19.5" y="864.3" class="legendText">
                          <tspan x="-19.5" y="864.3">{{ abbrNumber(sharedState.breaks[0]) }}</tspan>
                        </text>
                        <text y="864.4" x="28.6" class="legendText">
                          <tspan y="864.4" x="28.6">{{ abbrNumber(sharedState.breaks[1]) }}</tspan>
                        </text>
                        <text x="78.4" y="864.4" class="legendText">
                          <tspan x="78.4" y="864.4">{{ abbrNumber(sharedState.breaks[2]) }}</tspan>
                        </text>
                        <text y="864.4" x="128" class="legendText">
                          <tspan y="864.4" x="128">{{ abbrNumber(sharedState.breaks[3]) }}</tspan>
                        </text>
                        <text x="177.8" y="864.4" class="legendText">
                          <tspan x="177.8" y="864.4">{{ abbrNumber(sharedState.breaks[4]) }}</tspan>
                        </text>
                        <text y="864.3" x="225.8" class="legendText">
                          <tspan y="864.3" x="225.8">{{ abbrNumber(sharedState.breaks[5]) }}</tspan>
                        </text>
                    </g>
                </svg>

</template>

<template lang="html">

                swap_horiz
                swap_vert

{{ sharedState.title }}, {{ sharedState.year }}

 0">
                    SELECTED
                    {{ privateState.selected }}

                    COUNTY
                    {{ privateState.area }}

                 ({{ sharedState.metric.config.label.toLowerCase() }}).
                Total :
                 0" style="font-weight: bold; white-space: nowrap;">
                    Selected: {{privateState.selectedRaw}},

                    County: {{privateState.areaRaw}}

                <svg  v-if="sharedState.breaks" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet" viewBox="0 0 248.4 39.2"id="maplegend" role="img" aria-labelledby="svgTitle">
                    <title id="svgTitle">Choropleth legend</title>
                    <g transform="translate(20.714293 -851.75475)">
                        <rect y="865.9" x="-20.7" height="25" width="50" v-bind:style="{fill: this.sharedState.colors[0]}"/>
                        <rect width="50" height="25" x="28.9" y="865.9" v-bind:style="{fill: this.sharedState.colors[1]}"/>
                        <rect width="50" height="25" x="78.5" y="865.9" v-bind:style="{fill: this.sharedState.colors[2]}"/>
                        <rect y="865.9" x="128.1" height="25" width="50" v-bind:style="{fill: this.sharedState.colors[3]}"/>
                        <rect width="50" height="25" x="177.6" y="865.9" v-bind:style="{fill: this.sharedState.colors[4]}"/>
                        <text x="-19.5" y="864.3" class="legendText">
                          <tspan x="-19.5" y="864.3">{{ abbrNumber(sharedState.breaks[0]) }}</tspan>
                        </text>
                        <text y="864.4" x="28.6" class="legendText">
                          <tspan y="864.4" x="28.6">{{ abbrNumber(sharedState.breaks[1]) }}</tspan>
                        </text>
                        <text x="78.4" y="864.4" class="legendText">
                          <tspan x="78.4" y="864.4">{{ abbrNumber(sharedState.breaks[2]) }}</tspan>
                        </text>
                        <text y="864.4" x="128" class="legendText">
                          <tspan y="864.4" x="128">{{ abbrNumber(sharedState.breaks[3]) }}</tspan>
                        </text>
                        <text x="177.8" y="864.4" class="legendText">
                          <tspan x="177.8" y="864.4">{{ abbrNumber(sharedState.breaks[4]) }}</tspan>
                        </text>
                        <text y="864.3" x="225.8" class="legendText">
                          <tspan y="864.3" x="225.8">{{ abbrNumber(sharedState.breaks[5]) }}</tspan>
                        </text>
                    </g>
                </svg>

</template>


fix(carousel): Detect transitionend event name

Handle a few older browsers that may use prefixed `transitionend` event name

<template>

            <slot></slot>

        <template v-if="controls">

                {{labelPrev}}

                {{labelNext}}

        </template>

            <li v-for="n in slides.length"
                :key="'slide_' + n"
                role="button"
                :id="id ? (id + '__BV_indicator_' + n + '_') : null"
                :tabindex="indicators ? '0' : '-1'"
                :class="{active:n-1 === index}"
                :aria-current="n-1 === index ? 'true' : 'false'"
                :aria-label="labelGotoSlide + ' ' + n"
                :aria-describedby="slides[n-1].id || null"
                :aria-controls="id ? (id + '__BV_inner_') : null"
                @click="setSlide(n-1)"
                @keydown.enter.stop.prevent="setSlide(n-1)"
                @keydown.space.stop.prevent="setSlide(n-1)"
            >

</template>


Typo fix

Typo fix

<template>
    <transition
            enter-class=""
            enter-active-class="collapsing"
            enter-to-class=""
            leave-class=""
            leave-active-class="collapsing"
            leave-to-class=""
            @enter="onEnter"
            @after-enter="onAfterEnter"
            @leave="onLeave"
            @after-leave="onAfterLeave"
    >

<slot></slot>

    </transition>
</template>


chore: removed unused imports

chore: removed unused imports

<template>
  <b-nav-item-dropdown
        class="d-none d-sm-block"
        :text="lunchStore.selectedLocation.shortName"
  >
    <b-dropdown-item
          v-for="location in lunchStore.locations"
          :key="location.shortName"
          @click="lunchStore.mutateSelectedLocation(location)"
          :active="location === lunchStore.selectedLocation"
    >
      {{ location.name }}
    </b-dropdown-item>
  </b-nav-item-dropdown>
</template>

<script lang="ts">
import { Component, Vue, Inject } from 'vue-property-decorator'
import { LunchStore } from '@/store'

@Component
export default class LocationSelector extends Vue {
  @Inject() lunchStore!: LunchStore
}
</script>

<template>
  <b-nav-item-dropdown
        class="d-none d-sm-block"
        :text="lunchStore.selectedLocation.shortName"
  >
    <b-dropdown-item
          v-for="location in lunchStore.locations"
          :key="location.shortName"
          @click="lunchStore.mutateSelectedLocation(location)"
          :active="location === lunchStore.selectedLocation"
    >
      {{ location.name }}
    </b-dropdown-item>
  </b-nav-item-dropdown>
</template>

<script lang="ts">
import { Component, Vue, Inject } from 'vue-property-decorator'
import { getModule } from 'vuex-module-decorators'
import { LunchStore } from '@/store'

@Component
export default class LocationSelector extends Vue {
  @Inject() lunchStore!: LunchStore
}
</script>


Update Timeline

Update Timeline

<template>
  <q-page padding style="max-width: 800px">
    <q-timeline color="secondary" style="padding: 0 24px;">
      <q-timeline-entry heading>Timeline (3 media breakpoints)</q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="left"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 21, 1986"
        side="right"
        icon="delete"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry heading>November, 2017</q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="left"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="right"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="left"
        color="orange"
        icon="done_all"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="right"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="left"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>
    </q-timeline>

    <q-timeline color="red" dark class="bg-black" style="padding: 24px; margin-top: 50px;">
      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="left"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="right"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="left"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry heading>November, 2017</q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="left"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="right"
        icon="account_balance"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>
    </q-timeline>
  </q-page>
</template>

<template>
  <q-page padding style="max-width: 800px">
    <q-timeline color="secondary" style="padding: 0 24px;">
      <q-timeline-entry heading>Timeline (3 breakpoints)</q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="left"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry
        title="Event Title Event Title Event Title Event Title Event Title Event Title Event Title Event Title "
        subtitle="February 21, 1986"
        side="right"
        icon="delete"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry heading>November, 2017</q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="left"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="right"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="left"
        color="orange"
        icon="done_all"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="right"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="left"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>
    </q-timeline>

    <q-timeline color="red" dark class="bg-black" style="padding: 24px; margin-top: 50px;">
      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="left"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry
        title="Event Title Event Title Event Title Event Title Event Title Event Title Event Title Event Title "
        subtitle="February 22, 1986"
        side="right"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry
        title="Event Title Event Title Event Title Event Title Event Title Event Title Event Title Event Title "
        subtitle="February 22, 1986"
        side="left"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry heading>November, 2017</q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="left"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>

      <q-timeline-entry
        title="Event Title"
        subtitle="February 22, 1986"
        side="right"
        icon="account_balance"
      >

          Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      </q-timeline-entry>
    </q-timeline>
  </q-page>
</template>


修改下拉菜单内容高度

修改下拉菜单内容高度

<template>

                {{item.label}}

            <slot name="menu"></slot>

</template>


Remove nested title display.

Remove nested title display.


Add other relation types to default profile

<template>

                <Thing
                    :obj="framework"
                    :repo="repo"
                    :parentNotEditable="queryParams.view==='true'"
                    :profile="frameworkProfile" />
                {{ framework.competency.length }} item
                 1">{{ framework.competency.length }} items
                Last modified {{ lastModified }}
                Created {{ $moment(framework['schema:dateCreated']).fromNow() }}
                Approved
                Published
                <button
                    v-if="selectAllButton"
                    @click="selectAll=!selectAll">
                    Select All
                </button>
                <button
                    v-if="selectButtonText"
                    @click="selectButton">
                    {{ selectButtonText }}
                </button>

                <Hierarchy
                    :container="framework"
                    containerType="Framework"
                    containerTypeGet="EcFramework"
                    containerNodeProperty="competency"
                    containerEdgeProperty="relation"
                    nodeType="EcCompetency"
                    edgeType="EcAlignment"
                    edgeRelationProperty="relationType"
                    edgeRelationLiteral="narrows"
                    edgeSourceProperty="source"
                    edgeTargetProperty="target"
                    :editable="disallowEdits !== true && queryParams.view !== 'true'"
                    :repo="repo"
                    :queryParams="queryParams"
                    :exportOptions="competencyExportOptions"
                    :highlightList="highlightCompetency"
                    :selectMode="selectButtonText != null"
                    :selectAll="selectAll"
                    :profile="competencyProfile" />

</template>


fix: added ticks to timespiral, and other fixes

<template lang="pug">
div
  div
    | Events: {{ events.length }}
  svg#timespiral
</template>

<script lang="ts">
// This component renders an interactive "timespiral" where each turn of the "loop" is a 24h day.
// The spiral can be zoomed in/out, to display different days.
// Somewhat inspired by Apple's Time Machine UI.
//
// Similar things done with D3 here:
//  - http://bl.ocks.org/larsenmtl/222043d93a41d48b58d2bfa1e3d4f708
//  - https://observablehq.com/@mydu/spiral-arc-chart
//  - https://bl.ocks.org/tomshanley/4080b28445785939b3f043b8c5b63e22

import * as d3 from 'd3';
import { PieArcDatum } from 'd3-shape';
import moment from 'moment';
import { IEvent } from '~/util/interfaces';

function split_events_on_hours(events: IEvent[]): IEvent[] {
  // Takes a list of events, finds events that crosses hour-boundaries (e.g. 01:00, or 23:00)
  // and splits them into two (or more) events, one plus one per hour boundary crossed.
  //
  // Returns a list of events, with the original events intact, and the new events added.

  // Deepcopy
  events = JSON.parse(JSON.stringify(events));

  // First we need to find them, and filter them away from the other events.
  const crossing_events = events.filter(e => {
    // Does the event cross an hour mark?
    const start = moment(e.timestamp);
    const end = moment(e.timestamp).add(e.duration, 'seconds');
    return !start.isSame(end, 'hour');
  });

  const split_events = crossing_events
    .map(e => {
      // Split the event into at least two events, one more for each additional hour boundary crossed.
      const start = moment(e.timestamp);
      const end = moment(e.timestamp).add(e.duration, 'seconds');

      // We handle the start and end bits first, as they are not whole hours.
      const new_events: IEvent[] = [
        // First split event just needs to end at the end of the start hour.
        {
          ...e,
          duration: moment(start).startOf('hour').add(1, 'hour').diff(start, 'seconds'),
        },
        // Last split event just needs to start at the beginning of the end hour.
        {
          ...e,
          timestamp: moment(end).startOf('hour').format(),
          duration: moment(end).diff(end.startOf('hour'), 'seconds'),
        },
      ];

      // Now we handle the middle bits.
      for (
        let ts = start.startOf('hour').add(1, 'hour');
        ts < end.startOf('hour');
        ts.add(1, 'hour')
      ) {
        const new_e = {
          ...e,
          timestamp: ts.format(),
          duration: 60 * 60,
        };
        new_events.push(new_e);
      }

      return new_events;
    })
    .reduce((a, b) => a.concat(b), []); // flatten

  // Return the sorted list of: original events, without the crossing events, and with the new split events added.
  return (
    events
      .filter(e => !crossing_events.includes(e))
      .concat(split_events)
      // TypeScript won't let me sort this with -, for some reason
      .sort((a, b) => {
        const a_start = moment(a.timestamp);
        const b_start = moment(b.timestamp);
        return a_start.diff(b_start);
      })
      .reverse()
  );
}

export default {
  name: 'Timespiral',
  props: { events: { type: Array, default: () => [] } },
  data() {
    return {};
  },
  computed: {
    split_on_hour() {
      if (this.events && this.events.length > 0) {
        return split_events_on_hours(this.events);
      } else {
        return [];
      }
    },
  },
  watch: {
    events() {
      this.renderChart();
    },
  },
  mounted() {
    this.renderChart();
  },
  updated() {
    this.renderChart();
  },
  beforeUpdate() {
    const svg = d3.select('svg');
    svg.selectAll('*').remove();
  },
  methods: {
    renderChart() {
      if (this.events.length == 0) return;

      // Gets a deepcopy of events
      const events = this.split_on_hour;

      // Constants
      const height = 600;
      const width = 600;
      const margin = 60;
      const thickness = 30;

      // Init d3
      const svg = d3
        .select('svg#timespiral')
        .style('height', `${height}px`)
        .style('width', `${width}px`);

      const g = svg.append('g').attr('transform', `translate(${width / 2}, ${width / 2})`);

      // The domain is the range of the data.
      // We need to stretch it such that it ranges all the days in events, from start of day to end of day.
      const eventdomain = d3.extent(events.map((e: IEvent) => e.timestamp));
      const domain_start = moment(eventdomain[0]).startOf('day');
      const domain_end = moment(eventdomain[1]).endOf('day');
      const domain = [domain_start, domain_end];
      const nbSpirals = Math.ceil(
        (domain_end.valueOf() - domain_start.valueOf()) / (24 * 60 * 60 * 1000)
      );

      // To generate a spiral, we need two scales. One for the angle, and one for the radius.

      // The angle
      const xScale = d3
        .scaleTime()
        .domain(domain)
        .range([0, 2 * Math.PI * nbSpirals]);

      // The radius
      const yScale = d3
        .scaleTime()
        .domain(domain)
        .range([(width - margin) / 2 - (thickness + 20) * nbSpirals, (width - margin * 2) / 2]);

      // We want to add some spacing at hour-boundaries,
      // to do this we compress the startAngle and endAngle of each hour-segment, around the hour-center, by 1%.
      events.forEach(d => {
        d.startAngle = xScale(new Date(d.timestamp));
        d.endAngle = xScale(new Date(d.timestamp).valueOf() + 1000 * d.duration);

        // Compress the angles around the hour-center.
        const gap = 0.04;
        const hour = moment(d.timestamp).startOf('hour');

        // This kinda works? But also kinda breaks as it seems to lead to overlap
        // FIXME: Get rid of overlap bug
        d.startAngle += (d.startAngle - xScale(hour)) * gap;
        d.endAngle -= (d.endAngle - xScale(hour)) * gap;
      });

      // It seems very difficult to make proper smooth spiral arcs with D3.
      // Instead, we can split events on hour marks and leave a gap (helpful for readability).
      // Events within the same hour share the same radius/y, to give it the appearance of hour-segments.

      // Events have been split by `split_on_hour` above.

      // Computes the radius and spiral thickness for a particular event.
      // Each inner spiral (previous day) should get progressively thinner.
      function spiralThickness(e: IEvent): [number, number] {
        const hourstart = moment(e.timestamp).startOf('hour').valueOf();
        const angle = xScale(hourstart);
        const radius = yScale(hourstart);
        const spiralCount = angle / (2 * Math.PI); // the number of the spiral
        const scaling = Math.pow(spiralCount / nbSpirals, 0.3); // the scaling factor for the thickness
        console.log(`${e.timestamp} ${angle} ${radius} ${scaling}`);
        return [radius, thickness * scaling]; // the thickness of the spiral
      }

      const arcGen = d3
        .arc<PieArcDatum<number>>()
        // Compute the radius of each event, rounded to the hour in which the event occurs.
        .innerRadius(d => {
          const [radius, thick] = spiralThickness(events[d.data]);
          return radius - thick / 2;
        })
        .outerRadius(d => {
          const [radius, thick] = spiralThickness(events[d.data]);
          return radius + thick / 2;
        })
        // Makes corners round and pretty
        .cornerRadius(2);

      events.forEach((d: IEvent & { index: number; startAngle: number; endAngle: number }, i) => {
        //console.log(i, d);
        const gradientArcs = d3
          .pie<number>()
          .sort(null)
          .startAngle(d.startAngle)
          .endAngle(d.endAngle)
          .value(idx => {
            return moment(events[idx].timestamp).valueOf();
          })([i])
          .map(item => ({ ...item, index: i + item.index }));

        // Append the arc
        // TODO: Rotate arcs slightly so that arc ends are facing each other (is this even possible)
        g.append('g')
          .selectAll('path')
          .data(gradientArcs)
          .join('path')
          .attr('d', arcGen as any)
          .attr('stroke', 'none')
          // TODO: Colors should come from events (retrievable by query), not hardcoded
          .attr('fill', d.data.status == 'not-afk' ? '#0d0' : '#ccc')
          .attr('opacity', 0.7);
      });

      const tickColor = '#999';

      // Draw clock ticks
      // Modified from sunburst-clock.js
      function drawClockTick(group, a, radius) {
        const xn = Math.cos(a);
        const yn = Math.sin(a);

        // TODO: Use radius for the last event as a starting point,
        //       not the radius as if the day was full of events.
        //       Maybe even have the max radius be radius of the last event,
        //       with radius matching the last event for a certain time.
        group
          .append('line')
          .attr('x1', (radius - 5) * xn)
          .attr('y1', (radius - 5) * yn)
          .attr('x2', (radius + 5) * xn)
          .attr('y2', (radius + 5) * yn)
          .style('stroke', tickColor)
          .style('stroke-width', 2);
      }

      function drawClock(group, h, m, text, radius) {
        const a = 2 * Math.PI * (h / 24 + m / 24 / 60) - (1 / 2) * Math.PI;
        drawClockTick(g, a, radius);

        const xn = Math.cos(a);
        const yn = Math.sin(a);

        group
          .append('text')
          .text(text || moment({ hours: h }).format('HH:mm'))
          // Fall back to middle,
          // but should be right for 18:00 and left for 06:00.
          .attr('text-anchor', h == 6 ? 'start' : h == 18 ? 'end' : 'middle')
          .attr('dominant-baseline', 'middle')
          .attr('font-size', '1em')
          //.attr("font-weight", "bold")
          .style('fill', tickColor)
          .attr('x', (radius + 10) * xn)
          .attr('y', (radius + 20) * yn);
      }

      const radius = yScale(domain_end);
      drawClock(g, 0, 0, '00:00', radius);
      drawClock(g, 3, 0, '', radius);
      drawClock(g, 6, 0, '06:00', radius);
      drawClock(g, 9, 0, '', radius);
      drawClock(g, 12, 0, '12:00', radius);
      drawClock(g, 15, 0, '', radius);
      drawClock(g, 18, 0, '18:00', radius);
      drawClock(g, 21, 0, '', radius);

      const now = moment();
      drawClock(g, now.hour(), now.minute(), 'Now', radius);
    },
  },
};
</script>

<template lang="pug">
div
  div
    | Events: {{ events.length }}
  svg#timespiral
</template>

<script lang="ts">
// This component renders an interactive "timespiral" where each turn of the "loop" is a 24h day.
// The spiral can be zoomed in/out, to display different days.
// Somewhat inspired by Apple's Time Machine UI.
//
// Similar things done with D3 here:
//  - http://bl.ocks.org/larsenmtl/222043d93a41d48b58d2bfa1e3d4f708
//  - https://observablehq.com/@mydu/spiral-arc-chart
//  - https://bl.ocks.org/tomshanley/4080b28445785939b3f043b8c5b63e22

import * as d3 from 'd3';
import { PieArcDatum } from 'd3-shape';
import moment from 'moment';
import { IEvent } from '~/util/interfaces';

function split_events_on_hours(events: IEvent[]): IEvent[] {
  // Takes a list of events, finds events that crosses hour-boundaries (e.g. 01:00, or 23:00)
  // and splits them into two (or more) events, one plus one per hour boundary crossed.
  //
  // Returns a list of events, with the original events intact, and the new events added.

  // Deepcopy
  events = JSON.parse(JSON.stringify(events));

  // First we need to find them, and filter them away from the other events.
  const crossing_events = events.filter(e => {
    // Does the event cross an hour mark?
    const start = moment(e.timestamp);
    const end = moment(e.timestamp).add(e.duration, 'seconds');
    return !start.isSame(end, 'hour');
  });

  const split_events = crossing_events
    .map(e => {
      // Split the event into at least two events, one more for each additional hour boundary crossed.
      const start = moment(e.timestamp);
      const end = moment(e.timestamp).add(e.duration, 'seconds');

      // We handle the start and end bits first, as they are not whole hours.
      const new_events: IEvent[] = [
        // First split event just needs to end at the end of the start hour.
        {
          ...e,
          duration: moment(start).startOf('hour').add(1, 'hour').diff(start, 'seconds'),
        },
        // Last split event just needs to start at the beginning of the end hour.
        {
          ...e,
          timestamp: moment(end).startOf('hour').format(),
          duration: moment(end).diff(end.startOf('hour'), 'seconds'),
        },
      ];

      // Now we handle the middle bits.
      for (
        let ts = start.startOf('hour').add(1, 'hour');
        ts < end.startOf('hour');
        ts.add(1, 'hour')
      ) {
        const new_e = {
          ...e,
          timestamp: ts.format(),
          duration: 60 * 60,
        };
        new_events.push(new_e);
      }

      return new_events;
    })
    .reduce((a, b) => a.concat(b), []); // flatten

  // Return the sorted list of: original events, without the crossing events, and with the new split events added.
  return (
    events
      .filter(e => !crossing_events.includes(e))
      .concat(split_events)
      // TypeScript won't let me sort this with -, for some reason
      .sort((a, b) => {
        const a_start = moment(a.timestamp);
        const b_start = moment(b.timestamp);
        return a_start.diff(b_start);
      })
      .reverse()
  );
}

export default {
  name: 'Timespiral',
  props: { events: { type: Array, default: () => [] } },
  data() {
    return {};
  },
  computed: {
    split_on_hour() {
      if (this.events && this.events.length > 0) {
        return split_events_on_hours(this.events);
      } else {
        return [];
      }
    },
  },
  watch: {
    events() {
      this.renderChart();
    },
  },
  mounted() {
    this.renderChart();
  },
  updated() {
    this.renderChart();
  },
  beforeUpdate() {
    const svg = d3.select('svg');
    svg.selectAll('*').remove();
  },
  methods: {
    renderChart() {
      if (this.events.length == 0) return;

      // Gets a deepcopy of events
      const events = this.split_on_hour;

      // Constants
      const height = 500;
      const width = 500;
      const margin = 20;
      const thickness = 20;

      // Init d3
      const svg = d3
        .select('svg#timespiral')
        .style('height', `${height}px`)
        .style('width', `${width}px`);

      const g = svg.append('g').attr('transform', `translate(${width / 2}, ${width / 2})`);

      // The domain is the range of the data.
      // We need to stretch it such that it ranges all the days in events, from start of day to end of day.
      const eventdomain = d3.extent(events.map((e: IEvent) => e.timestamp));
      const domain_start = moment(eventdomain[0]).startOf('day');
      const domain_end = moment(eventdomain[1]).endOf('day');
      const domain = [domain_start, domain_end];
      const nbSpirals = Math.ceil(
        (domain_end.valueOf() - domain_start.valueOf()) / (24 * 60 * 60 * 1000)
      );

      // To generate a spiral, we need two scales. One for the angle, and one for the radius.

      // The angle
      const xScale = d3
        .scaleTime()
        .domain(domain)
        .range([0, 2 * Math.PI * nbSpirals]);

      // The radius
      const yScale = d3
        .scaleTime()
        .domain(domain)
        .range([(width - margin) / 2 - 30 * nbSpirals, (width - margin * 2) / 2]);

      // We want to add some spacing at hour-boundaries,
      // to do this we compress the startAngle and endAngle of each hour-segment, around the hour-center, by 1%.
      events.forEach(d => {
        d.startAngle = xScale(new Date(d.timestamp));
        d.endAngle = xScale(new Date(d.timestamp).valueOf() + 1000 * d.duration);

        // Compress the angles around the hour-center.
        const gap = 0.04;
        const hour = moment(d.timestamp).startOf('hour');

        // This kinda works? But also kinda breaks as it seems to lead to overlap
        // FIXME: Get rid of overlap bug
        d.startAngle += (d.startAngle - xScale(hour)) * gap;
        d.endAngle -= (d.endAngle - xScale(hour)) * gap;
      });

      // It seems very difficult to make proper smooth spiral arcs with D3.
      // Instead, we can split events on hour marks and leave a gap (helpful for readability).
      // Events within the same hour share the same radius/y, to give it the appearance of hour-segments.

      // Events have been split by `split_on_hour` above.

      const arcGen = d3
        .arc<PieArcDatum<number>>()
        // Compute the radius of each event, rounded to the hour in which the event occurs.
        .innerRadius(d => {
          return yScale(moment(events[d.data].timestamp).startOf('hour').valueOf()) - thickness / 2;
        })
        .outerRadius(
          d => yScale(moment(events[d.data].timestamp).endOf('hour').valueOf()) + thickness / 2
        )
        // Makes corners round and pretty
        .cornerRadius(3);

      events.forEach((d: IEvent & { index: number; startAngle: number; endAngle: number }, i) => {
        //console.log(i, d);
        const gradientArcs = d3
          .pie<number>()
          .sort(null)
          .startAngle(d.startAngle)
          .endAngle(d.endAngle)
          .value(idx => {
            return moment(events[idx].timestamp).valueOf();
          })([i])
          .map(item => ({ ...item, index: i + item.index }));

        // Append the arc
        // TODO: Rotate arcs slightly so that arc ends are facing each other (is this even possible)
        g.append('g')
          .selectAll('path')
          .data(gradientArcs)
          .join('path')
          .attr('d', arcGen as any)
          .attr('stroke', 'none')
          // TODO: Colors should come from events (retrievable by query), not hardcoded
          .attr('fill', d.data.status == 'not-afk' ? '#0f0' : '#f00')
          .attr('opacity', 0.5);
      });
    },
  },
};
</script>


update

update