diff --git a/gulpfile.js b/gulpfile.js
index 4ce81f32d8e17aa66fa73009def2a78b77d6c6a1..8c7b7af394123119164838581bd15800f78e0ab3 100755
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -419,7 +419,7 @@ gulp.task('rollup:build:production', buildJSBundle("production"));
 
 gulp.task(
   "site:build",
-  gulp.parallel("tailwind-postcss:build", "rollup:build")
+  gulp.parallel("tailwind-postcss:build", "patternlab:build", "rollup:build")
 );
 
 gulp.task(
diff --git a/source/_meta/_01-foot.mustache b/source/_meta/_01-foot.mustache
index 3e8c0ec5fdc931b9fd03281657ccf486e4a12a49..35f1de8d29e45318fc592a42adad704d566a0831 100755
--- a/source/_meta/_01-foot.mustache
+++ b/source/_meta/_01-foot.mustache
@@ -2,7 +2,7 @@
   <!--DO NOT REMOVE-->
   {{{ patternLabFoot }}}
 
-  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js"></script>
+  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script>
   <script src="../../js/main.bundle.js?{{ cacheBuster }}"></script>
 
   <script>
diff --git a/source/_patterns/00-atoms/02-contact-lines/basic-contact-line.mustache b/source/_patterns/00-atoms/02-contact-lines/basic-contact-line.mustache
index 4241e74ede404f7f1f9da93ff180a1021feeff0a..1bfdb533a2d568a68941aa6043999096ecfc6bdb 100644
--- a/source/_patterns/00-atoms/02-contact-lines/basic-contact-line.mustache
+++ b/source/_patterns/00-atoms/02-contact-lines/basic-contact-line.mustache
@@ -1,3 +1,4 @@
-<a href="mailto:example@example.com" class="contact-line {{ classes }}">
-  <i class="{{ icon }}"></i>{{ caption }}{{^caption}}example@example.com{{/caption}}
+<a href="mailto:example@example.com" class="icon-link contact-line {{ classes }}">
+  <i class="{{ icon }}"></i>
+  <span>{{ caption }}{{^caption}}example@example.com{{/caption}}</span>
 </a>
diff --git a/source/_patterns/01-molecules/calendar/calendar-table-row.mustache b/source/_patterns/01-molecules/calendar/calendar-table-row.mustache
index 7296b75802b82b398320464a2229ad6941ddec75..761b689727cc69be564cad69b02e283c8124c5ad 100644
--- a/source/_patterns/01-molecules/calendar/calendar-table-row.mustache
+++ b/source/_patterns/01-molecules/calendar/calendar-table-row.mustache
@@ -11,7 +11,7 @@
     </div>
   </div>
   <div class="col-span-2 text-center font-light calendar-table-row__col">
-    <a href="#">
+    <a href="#" class="icon-link">
       <i class="ico--location text-violet-300 mr-1" aria-hidden="true"></i>
       <span>Mapa</span>
     </a>
diff --git a/source/_patterns/01-molecules/calendar/js-calendar.mustache b/source/_patterns/01-molecules/calendar/js-calendar.mustache
new file mode 100644
index 0000000000000000000000000000000000000000..2871bce7c13eb07089fe419bf22be866b458210f
--- /dev/null
+++ b/source/_patterns/01-molecules/calendar/js-calendar.mustache
@@ -0,0 +1,7 @@
+<div class="__js-root">
+  <ui-app inline-template>
+    <ui-calendar-dummy-provider v-slot="{ events, onShowMore, hasMore }">
+      <ui-calendar-renderer :events="events" :on-show-more="onShowMore" :has-more="hasMore" :name="'Krajský kalendář'"></ui-calendar-renderer>
+    </ui-calendar-dummy-provider>
+  </ui-app>
+</div>
diff --git a/source/_patterns/01-molecules/flip-clock/flip-clock.mustache b/source/_patterns/01-molecules/flip-clock/flip-clock.mustache
index 94bf274c5faebc560d085214ccfbbed0ec352af8..8f4fea9fb4923d5272f1da775dde3370f6a2561f 100644
--- a/source/_patterns/01-molecules/flip-clock/flip-clock.mustache
+++ b/source/_patterns/01-molecules/flip-clock/flip-clock.mustache
@@ -1,7 +1,8 @@
-<div class="flip-clock __js-root"
-  data-app="FlipClock"
-  data-clockclasses="{{ clockClasses }}"
-  data-slotclasses="{{ slotClasses }}"
-  data-deadline="2020-12-10 00:00:00"
-  {{# units }}data-units="{{ units }}"{{/ units }}
-></div>
+<div class="flip-clock __js-root">
+  <ui-flip-clock
+    clock-classes="{{ clockClasses }}"
+    slot-classes="{{ slotClasses }}"
+    deadline="2020-12-10 00:00:00"
+    {{# units }}units="{{ units }}"{{/ units }}
+  />
+</div>
diff --git a/source/_patterns/02-organisms/00-global/footer.mustache b/source/_patterns/02-organisms/00-global/footer.mustache
index 515799effcca3a069a939eb9ea1e65cc083cd580..79dc32f58d9175a7735b8c99c9a399585c1aabf4 100644
--- a/source/_patterns/02-organisms/00-global/footer.mustache
+++ b/source/_patterns/02-organisms/00-global/footer.mustache
@@ -1,5 +1,5 @@
-<footer class="footer bg-grey-700 text-white __js-root" data-app="Footer">
-  <ui-footer inline-template>
+<footer class="footer bg-grey-700 text-white __js-root">
+  <ui-app inline-template>
     <div>
       <div class="footer__main py-4 lg:py-16 container container--default">
         <section class="footer__brand">
@@ -101,5 +101,5 @@
         </div>
       </section>
     </div>
-  </ui-footer>
+  </ui-app>
 </footer>
diff --git a/source/_patterns/02-organisms/00-global/navbar.mustache b/source/_patterns/02-organisms/00-global/navbar.mustache
index 833bf250c5476d1577d6a83f9dc9b5de86fae294..45c7918ff86cd95c9f3225f0b0f58182ad35376a 100644
--- a/source/_patterns/02-organisms/00-global/navbar.mustache
+++ b/source/_patterns/02-organisms/00-global/navbar.mustache
@@ -1,65 +1,59 @@
-<nav class="navbar __js-root" data-app="Navbar">
-  <ui-navbar inline-template>
-    <div>
-      <div class="container container--wide navbar__content" :class="{'navbar__content--initialized': true}">
-        <div class="navbar__brand my-4 flex items-center lg:block lg:pr-8 lg:my-0">
-          <a href="#">
-            <img src="/images/logo-round-white.svg" class="w-8 lg:w-full lg:border-r lg:border-grey-300 lg:pr-8" />
-          </a>
-          <span class="lg:hidden pl-4 font-bold text-xl">Pirátská strana</span>
-        </div>
-        <div class="navbar__menutoggle my-4 flex justify-end lg:hidden">
-          <a href="#" @click="show = !show" class="no-underline hover:no-underline">
-            <i class="ico--menu text-3xl"></i>
-          </a>
-        </div>
-        <div v-if="show || isLgScreenSize" class="navbar__external navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto lg:flex lg:space-x-8 lg:pb-2">
-          <div class="text-grey-200 text-sm lg:space-x-8 leading-loose lg:leading-normal">
-            {{> atoms-basic-contact-line(caption: "pirati.cz", icon: "ico--home", classes: "block lg:inline-block") }}
-            {{> atoms-basic-contact-line(caption: "piratskyobchod.cz", icon: "ico--cart", classes: "block lg:inline-block") }}
-            {{> atoms-basic-contact-line(caption: "piratskelisty.cz", icon: "ico--newspaper", classes: "block lg:inline-block") }}
+<nav class="navbar __js-root">
+  <ui-app inline-template>
+    <ui-navbar inline-template>
+      <div>
+        <div class="container container--wide navbar__content" :class="{'navbar__content--initialized': true}">
+          <div class="navbar__brand my-4 flex items-center lg:block lg:pr-8 lg:my-0">
+            <a href="#">
+              <img src="/images/logo-round-white.svg" class="w-8 lg:w-full lg:border-r lg:border-grey-300 lg:pr-8" />
+            </a>
+            <span class="lg:hidden pl-4 font-bold text-xl">Pirátská strana</span>
+          </div>
+          <div class="navbar__menutoggle my-4 flex justify-end lg:hidden">
+            <a href="#" @click="show = !show" class="no-underline hover:no-underline">
+              <i class="ico--menu text-3xl"></i>
+            </a>
+          </div>
+          <div v-if="show || isLgScreenSize" class="navbar__external navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto lg:flex lg:space-x-8 lg:pb-2">
+            <div class="text-grey-200 text-sm lg:space-x-8 leading-loose lg:leading-normal">
+              {{> atoms-basic-contact-line(caption: "pirati.cz", icon: "ico--home", classes: "block lg:inline-block") }}
+              {{> atoms-basic-contact-line(caption: "piratskyobchod.cz", icon: "ico--cart", classes: "block lg:inline-block") }}
+              {{> atoms-basic-contact-line(caption: "piratskelisty.cz", icon: "ico--newspaper", classes: "block lg:inline-block") }}
+            </div>
+            {{> molecules-basic-social-icon-group(classes: "text-grey-200 py-4 lg:py-0") }}
+          </div>
+          <div v-if="show || isLgScreenSize" class="navbar__main navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto">
+            <ul class="navbar-menu text-white">
+              <li class="navbar-menu__item">
+                <a href="#" data-href="{{ link.templates-homepage }}" class="navbar-menu__link">Hlavní strana</a>
+              </li>
+              <li class="navbar-menu__item">
+                <a href="#" data-href="{{ link.templates-people }}" class="navbar-menu__link">Lidé</a>
+              </li>
+              <li class="navbar-menu__item">
+                <ui-navbar-subitem label="Aktuality" href="{{ link.templates-article-listing }}">
+                  <ul class="navbar-menu__submenu">
+                    <li><a href="#" data-href="{{ link.templates-article-detail }}" class="navbar-menu__link">Detail aktuality</a></li>
+                  </ul>
+                </ui-navbar-subtitem>
+              </li>
+              <li class="navbar-menu__item">
+                <a href="#" data-href="{{ link.templates-elections }}" class="navbar-menu__link">Volby</a>
+              </li>
+              <li class="navbar-menu__item">
+                <a href="#" data-href="{{ link.templates-pirate-center }}" class="navbar-menu__link">Pirátské centrum</a>
+              </li>
+              <li class="navbar-menu__item">
+                <a href="#" data-href="{{ link.templates-contact }}" class="navbar-menu__link">Kontakt</a>
+              </li>
+            </ul>
+          </div>
+          <div v-if="show || isLgScreenSize" class="navbar__actions navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto self-start flex flex-col sm:flex-row lg:flex-col sm:space-x-4 space-y-2 sm:space-y-0 lg:space-y-2 xl:flex-row xl:space-x-2 xl:space-y-0">
+            {{> atoms-icon-button(cta: "Přispěj", icon: "ico--pig", classes: "btn--cyan-200 btn--hoveractive btn--condensed btn--fullwidth md:btn--autowidth lg:text-sm xl:text-base") }}
+            {{> atoms-icon-button(cta: "Naloď se", icon: "ico--anchor", classes: "btn--blue-300 btn--hoveractive btn--condensed btn--fullwidth md:btn--autowidth lg:text-sm xl:text-base") }}
           </div>
-          {{> molecules-basic-social-icon-group(classes: "text-grey-200 py-4 lg:py-0") }}
-        </div>
-        <div v-if="show || isLgScreenSize" class="navbar__main navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto">
-          <ul class="navbar-menu text-white">
-            <li class="navbar-menu__item">
-              <a href="#" data-href="{{ link.templates-homepage }}" class="navbar-menu__link">Hlavní strana</a>
-            </li>
-            <li class="navbar-menu__item">
-              <a href="#" data-href="{{ link.templates-people }}" class="navbar-menu__link">Lidé</a>
-            </li>
-            <li class="navbar-menu__item">
-              <ui-navbar-subitem label="Aktuality" href="{{ link.templates-article-listing }}">
-                <ul class="navbar-menu__submenu">
-                  <li><a href="#" data-href="{{ link.templates-article-detail }}" class="navbar-menu__link">Detail aktuality</a></li>
-                </ul>
-              </ui-navbar-subtitem>
-            </li>
-            <li class="navbar-menu__item">
-              <ui-navbar-subitem label="Volby">
-                <ul class="navbar-menu__submenu">
-                  <li><a href="#" data-href="{{ link.templates-elections-candidates }}" class="navbar-menu__link">Kandidáti</a></li>
-                  <li><a href="#" data-href="{{ link.templates-elections-program }}" class="navbar-menu__link">Program</a></li>
-                </ul>
-              </ui-navbar-subtitem>
-            </li>
-            <li class="navbar-menu__item">
-              <a href="#" data-href="{{ link.templates-elections-candidates }}" class="navbar-menu__link">Kandidáti</a>
-            </li>
-            <li class="navbar-menu__item">
-              <a href="#" data-href="{{ link.templates-pirate-center }}" class="navbar-menu__link">Pirátské centrum</a>
-            </li>
-            <li class="navbar-menu__item">
-              <a href="#" data-href="{{ link.templates-contact }}" class="navbar-menu__link">Kontakt</a>
-            </li>
-          </ul>
-        </div>
-        <div v-if="show || isLgScreenSize" class="navbar__actions navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto self-start flex flex-col sm:flex-row lg:flex-col sm:space-x-4 space-y-2 sm:space-y-0 lg:space-y-2 xl:flex-row xl:space-x-2 xl:space-y-0">
-          {{> atoms-icon-button(cta: "Přispěj", icon: "ico--pig", classes: "btn--cyan-200 btn--hoveractive btn--condensed btn--fullwidth md:btn--autowidth lg:text-sm xl:text-base") }}
-          {{> atoms-icon-button(cta: "Naloď se", icon: "ico--anchor", classes: "btn--blue-300 btn--hoveractive btn--condensed btn--fullwidth md:btn--autowidth lg:text-sm xl:text-base") }}
         </div>
       </div>
-    </div>
-  </ui-navbar>
+    </ui-navbar>
+  </ui-app>
 </nav>
diff --git a/source/_patterns/02-organisms/00-global/subnav.mustache b/source/_patterns/02-organisms/00-global/subnav.mustache
index 9680ac01689de9f1d2afad2b5faf97dd0f464d29..52ef2e0f6da1367066ddf1f20e35249c0c1cf7b3 100644
--- a/source/_patterns/02-organisms/00-global/subnav.mustache
+++ b/source/_patterns/02-organisms/00-global/subnav.mustache
@@ -1,6 +1,6 @@
-<div class="__js-root" data-app="Subnav">
-  <ui-subnav inline-template>
-    <ui-subnav-view-provider :initial="{regions: false, calendar: false}" v-slot="{ isCurrentView, toggleView }">
+<div class="__js-root">
+  <ui-app inline-template>
+    <ui-view-provider :initial="{regions: false, calendar: false}" v-slot="{ isCurrentView, toggleView }">
       <nav class="subnav">
         <div class="container container--wide">
           <div class="flex">
@@ -45,15 +45,17 @@
       <aside class="subnav-aside">
         <div class="subnav-aside__item" :class="{'subnav-aside__item--visible': isCurrentView('regions')}">
           <a @click="toggleView('regions')" class="subnav-aside__close"><i class="ico--close"></i></a>
-          <ui-region-map class="container container--default" />
+          <ui-region-map class="container container--default"></ui-region-map>
         </div>
         <div class="subnav-aside__item" :class="{'subnav-aside__item--visible': isCurrentView('calendar')}">
           <a @click="toggleView('calendar')" class="subnav-aside__close"><i class="ico--close"></i></a>
           <div class="container container--default">
-            {{> molecules-calendar }}
+            <ui-calendar-dummy-provider v-slot="{ events, onShowMore, hasMore }">
+              <ui-calendar-renderer :events="events" :on-show-more="onShowMore" :has-more="hasMore" :name="'Krajský kalendář'"></ui-calendar-renderer>
+            </ui-calendar-dummy-provider>
           </div>
         </div>
       </aside>
-    </ui-subnav-view-provider>
-  </ui-subnav>
+    </ui-view-provider>
+  </ui-app>
 </div>
diff --git a/source/_patterns/02-organisms/region-map/region-map.mustache b/source/_patterns/02-organisms/region-map/region-map.mustache
index 5637b6a669c2dbc1c882c49752756313617c717a..06b027af672a704d76c2894b9b4dafd8bda358ef 100644
--- a/source/_patterns/02-organisms/region-map/region-map.mustache
+++ b/source/_patterns/02-organisms/region-map/region-map.mustache
@@ -1 +1,3 @@
-<div class="region-map __js-root" data-app="RegionMap"></div>
+<div class="__js-root">
+  <ui-region-map />
+</div>
diff --git a/source/_patterns/03-templates/elections.mustache b/source/_patterns/03-templates/elections.mustache
index 74d7173fabbe5e33092d0a98c147a0933ff82be5..a5e6ce139c35cd38bddeba123bd610f6ad9d5cee 100644
--- a/source/_patterns/03-templates/elections.mustache
+++ b/source/_patterns/03-templates/elections.mustache
@@ -1,8 +1,8 @@
 {{> organisms-header }}
 {{> organisms-elections-hero }}
 
-<div class="__js-root" data-app="ViewProvider">
-  <ui-view-provider inline-template :initial="{candidates: true, program: false}" v-slot="{ isCurrentView, toggleView }">
+<div class="__js-root">
+  <ui-view-provider :initial="{candidates: true, program: false}" v-slot="{ isCurrentView, toggleView }">
     <main>
       <div class="container container--default pt-8 lg:py-24">
         <div class="text-center">
diff --git a/source/css/atoms/icons.pcss b/source/css/atoms/icons.pcss
index ad253e9fef822965f32ed864e180370be6bd41df..b6e1e7213a09cc6390183693955a0473d1032ea9 100644
--- a/source/css/atoms/icons.pcss
+++ b/source/css/atoms/icons.pcss
@@ -1,3 +1,4 @@
+[class^="ico--"],
 [class^="ico--"]:before,
 [class*=" ico--"]:before,
 [class^="ico--"]:hover:before,
diff --git a/source/css/molecules/calendar.pcss b/source/css/molecules/calendar.pcss
index 84ae5bfc614f1404a9604b149ab08dee83d3e19e..dff5a32e8ca805a5aa0ba2301713f73c8a7f47db 100644
--- a/source/css/molecules/calendar.pcss
+++ b/source/css/molecules/calendar.pcss
@@ -21,4 +21,7 @@
     }
   }
 
+  &.calendar-table-row__col--norborder {
+    @apply border-r-0;
+  }
 }
diff --git a/source/css/style.pcss b/source/css/style.pcss
index 855c8781c6a0206d0630e69ee1b9b2f37a4f0ad6..0147315698ffb95be854e3dc95f28eedd565a3e5 100644
--- a/source/css/style.pcss
+++ b/source/css/style.pcss
@@ -81,3 +81,12 @@ body {
 a:hover {
   @apply underline;
 }
+
+a.icon-link:hover {
+  @apply no-underline;
+
+  span {
+    @apply underline;
+  }
+}
+
diff --git a/source/js/apps.js b/source/js/apps.js
deleted file mode 100644
index 54d6011a1ffc51dc7f5c88e60873e5ad8058dff0..0000000000000000000000000000000000000000
--- a/source/js/apps.js
+++ /dev/null
@@ -1,16 +0,0 @@
-
-import FlipClock from './apps/flip-clock';
-import Footer from './apps/footer';
-import Navbar from './apps/navbar';
-import RegionMap from './apps/region-map';
-import Subnav from './apps/subnav';
-import ViewProvider from './apps/view-provider';
-
-export default {
-  FlipClock,
-  Footer,
-  Navbar,
-  RegionMap,
-  Subnav,
-  ViewProvider
-};
diff --git a/source/js/apps/flip-clock/index.js b/source/js/apps/flip-clock/index.js
deleted file mode 100644
index e66cd7b7e0addc12352681ab94049efbeecdabd5..0000000000000000000000000000000000000000
--- a/source/js/apps/flip-clock/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import Vue from 'vue';
-import FlipClock from './FlipClock.vue';
-
-const appFactory = (el, attrs) => {
-  // Bootstrap Vue.js.
-  new Vue({el, render: h => h(FlipClock, { attrs })});
-};
-
-export default appFactory;
diff --git a/source/js/apps/footer/Footer.vue b/source/js/apps/footer/Footer.vue
deleted file mode 100644
index 504edc15ed53b604a45a042fb0f2dab25d275498..0000000000000000000000000000000000000000
--- a/source/js/apps/footer/Footer.vue
+++ /dev/null
@@ -1,9 +0,0 @@
-<script>
-import UiFooterCollapsible from "./FooterCollapsible";
-
-export default {
-  components: {
-    UiFooterCollapsible,
-  },
-}
-</script>
diff --git a/source/js/apps/footer/index.js b/source/js/apps/footer/index.js
deleted file mode 100644
index 656a6be9d8d49eddfb8c97e5b03359ebfcaa03fa..0000000000000000000000000000000000000000
--- a/source/js/apps/footer/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import Vue from 'vue';
-import UiFooter from './Footer.vue';
-
-const appFactory = (el, attrs) => {
-  // Bootstrap Vue.js.
-  new Vue({el, components: { UiFooter }});
-};
-
-export default appFactory;
diff --git a/source/js/apps/navbar/index.js b/source/js/apps/navbar/index.js
deleted file mode 100644
index 56ce38c44b14c95328ccf8d24744f81ac3b8a1c6..0000000000000000000000000000000000000000
--- a/source/js/apps/navbar/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import Vue from 'vue';
-import UiNavbar from './Navbar.vue';
-
-const appFactory = (el, attrs) => {
-  // Bootstrap Vue.js.
-  new Vue({el, components: { UiNavbar }});
-};
-
-export default appFactory;
diff --git a/source/js/apps/region-map/index.js b/source/js/apps/region-map/index.js
deleted file mode 100644
index a79b09c7bd7b2bdfbef78c6216148272666a820e..0000000000000000000000000000000000000000
--- a/source/js/apps/region-map/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import Vue from 'vue';
-import UiRegionMap from './RegionMap.vue';
-
-const appFactory = (el, attrs) => {
-  // Bootstrap Vue.js.
-  new Vue({el, render: h => h(UiRegionMap, { attrs })});
-};
-
-export default appFactory;
diff --git a/source/js/apps/subnav/index.js b/source/js/apps/subnav/index.js
deleted file mode 100644
index bf9c0653e71b5041513ae0a62ee967bdc996ec53..0000000000000000000000000000000000000000
--- a/source/js/apps/subnav/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import Vue from 'vue';
-import UiSubnav from './Subnav.vue';
-
-const appFactory = (el, attrs) => {
-  // Bootstrap Vue.js.
-  new Vue({el, components: { UiSubnav }});
-};
-
-export default appFactory;
diff --git a/source/js/apps/view-provider/index.js b/source/js/apps/view-provider/index.js
deleted file mode 100644
index 6205adb51543824794aa7d7f7141ac4ce4b39b1c..0000000000000000000000000000000000000000
--- a/source/js/apps/view-provider/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import Vue from 'vue';
-import UiViewProvider from '../../components/ViewProvider.vue';
-
-const appFactory = (el, attrs) => {
-  // Bootstrap Vue.js.
-  new Vue({el, components: { UiViewProvider }});
-};
-
-export default appFactory;
diff --git a/source/js/apps/flip-clock/FlipClock.vue b/source/js/components/FlipClock.vue
similarity index 96%
rename from source/js/apps/flip-clock/FlipClock.vue
rename to source/js/components/FlipClock.vue
index 619e3d777e29fd74651e1248d5987fcfcc66361c..81a1687386244c438f5c23ed4ca54a6685323c86 100644
--- a/source/js/apps/flip-clock/FlipClock.vue
+++ b/source/js/components/FlipClock.vue
@@ -2,13 +2,13 @@
   <div class="flip-clock">
     <template v-for="data in timeData" v-show="show">
       <span v-bind:key="data.label" class="flip-clock__piece" :id="data.elementId" v-show="data.show">
-        <span :class="['flip-clock__card', 'flip-card', clockclasses ]">
+        <span :class="['flip-clock__card', 'flip-card', clockClasses ]">
           <b class="flip-card__top">{{ data.current | twoDigits }}</b>
           <b class="flip-card__bottom" v-bind:data-value="data.current | twoDigits"></b>
           <b class="flip-card__back" v-bind:data-value="data.previous | twoDigits"></b>
           <b class="flip-card__back-bottom" v-bind:data-value="data.previous | twoDigits"></b>
         </span>
-        <span :class="['flip-clock__slot', 'font-alt', slotclasses]">{{ data.label }}</span>
+        <span :class="['flip-clock__slot', 'font-alt', slotClasses]">{{ data.label }}</span>
       </span>
     </template>
   </div>
@@ -16,7 +16,7 @@
 
 <script>
 import Vue from "vue";
-import { forEachNode } from "../../utils";
+import { forEachNode } from "../utils";
 
 export default {
   name: 'flipCountdown',
@@ -31,11 +31,11 @@ export default {
       type: String,
       default: 'days,hours,minutes,seconds'
     },
-    clockclasses: {
+    clockClasses: {
       type: String,
       default: 'text-6xl'
     },
-    slotclasses: {
+    slotClasses: {
       type: String,
       default: 'text-3xl'
     }
diff --git a/source/js/apps/region-map/RegionMap.vue b/source/js/components/RegionMap.vue
similarity index 96%
rename from source/js/apps/region-map/RegionMap.vue
rename to source/js/components/RegionMap.vue
index fd5d660fff9122bdfd5b1b73471fd0f3abd4897a..ebff74871494489d53631ae2999cbc696070a97b 100644
--- a/source/js/apps/region-map/RegionMap.vue
+++ b/source/js/components/RegionMap.vue
@@ -4,7 +4,7 @@
       <h1 class="head-alt-sm mb-2">Vyberte kraj</h1>
       <ul class="region-map__list leading-loose whitespace-no-wrap text-sm">
         <li v-for="region in regions" :key="region.id">
-          <a href="#" @mouseover="current = region" @mouseout="current = null">{{ region.name }}</a>
+          <a href="#" @click="selectRegion(region)" @mouseover="current = region" @mouseout="current = null">{{ region.name }}</a>
         </li>
       </ul>
     </div>
@@ -17,7 +17,7 @@
         viewBox="0 75 800 450"
       >
         <g>
-          <a xlink:href="#" v-for="region in regions" :key="region.id" @mouseover="current = region" @mouseout="current = null">
+          <a xlink:href="#" v-for="region in regions" :key="region.id" @mouseover="current = region" @mouseout="current = null" @click="selectRegion(region)">
             <path
               :class="{'region-map__region': true, 'region-map__region--current': current === region}"
               :d="region.polygon"
@@ -34,7 +34,33 @@ import Vue from "vue";
 
 export default {
   props: {
-
+    links: {
+      type: Object,
+      default: function () {
+        return {
+          "praha": "https://praha.pirati.cz",
+          "stredocesky": "https://stredocesky.pirati.cz",
+          "jihocesky": "https://jihocesky.pirati.cz",
+          "plzensky": "https://plzensky.pirati.cz",
+          "karlovarsky": "https://karlovarsky.pirati.cz",
+          "ustecky": "https://ustecky.pirati.cz",
+          "liberecky": "https://liberecky.pirati.cz",
+          "kralovehradecky": "https://kralovehradecky.pirati.cz",
+          "moravskoslezsky": "https://moravskoslezsky.pirati.cz",
+          "pardubicky": "https://pardubicky.pirati.cz",
+          "vysocina": "https://vysocina.pirati.cz",
+          "jihomoravsky": "https://jihomoravsky.pirati.cz",
+          "olomoucky": "https://olomoucky.pirati.cz",
+          "zlinsky": "https://zlinsky.pirati.cz"
+        };
+      }
+    }
+  },
+  methods: {
+    selectRegion(region) {
+      const href = this.$props.links[region.id];
+      window.location.href = href;
+    }
   },
   data() {
     return {
diff --git a/source/js/apps/subnav/Subnav.vue b/source/js/components/Subnav.vue
similarity index 100%
rename from source/js/apps/subnav/Subnav.vue
rename to source/js/components/Subnav.vue
diff --git a/source/js/components/UiApp.vue b/source/js/components/UiApp.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ec184d3577114d7701b2e3ff13b59f2fbfcecc47
--- /dev/null
+++ b/source/js/components/UiApp.vue
@@ -0,0 +1,7 @@
+<script>
+export default {
+  mounted() {
+    console.log(`Mounted generic Vue app in ` , this.$el);
+  }
+}
+</script>
diff --git a/source/js/components/calendar/DummyProvider.vue b/source/js/components/calendar/DummyProvider.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d01a6aec9f1f9121f1da8861f29e1473e892138b
--- /dev/null
+++ b/source/js/components/calendar/DummyProvider.vue
@@ -0,0 +1,161 @@
+<script>
+const initialEvents = [
+  {
+    id: 2,
+    start: new Date("2020-07-08T10:00:00.000Z"),
+    startDateVerbose: "středa 8. července 2020",
+    startTimeVerbose: "12:00",
+    allDay: false,
+    end: new Date("2020-07-08T11:00:00.000Z"),
+    title: "Pirátský oběd - Chrudim",
+    description:
+      "Pravidelné setkání pirátů při středečním obědě. Nejen o politice a s chutí.",
+    link:
+      "https://www.google.com/calendar/event?eid=Mmw1Y2RwMTByYm80Y204cWxsaW1maWJmcTJfMjAyMDA3MDhUMTAwMDAwWiA3cjY3M3JsaDI1NW9mb3JodjNvZWIybDBnMEBn"
+  },
+  {
+    id: 15,
+    start:  new Date("2020-07-13T19:00:00.000Z"),
+    startDateVerbose: "pondělí 13. července 2020",
+    startTimeVerbose: "21:00",
+    allDay: false,
+    end: new Date("2020-07-13T19:30:00.000Z"),
+    title: "Mumble - předsednictvo",
+    link:
+      "https://www.google.com/calendar/event?eid=YzVpM2FvaGc2MHAzY2I5aGM1aW1jYjlrNjBvbThiYjE2dGk2NGI5ajY4cjY0ZGhrNzVnamdjOWdjb18yMDIwMDcxM1QxOTAwMDBaIDdyNjczcmxoMjU1b2Zvcmh2M29lYjJsMGcwQGc"
+  },
+  {
+    id: 3,
+    start: new Date("2020-07-15T10:00:00.000Z"),
+    startDateVerbose: "středa 15. července 2020",
+    startTimeVerbose: "12:00",
+    allDay: false,
+    end: new Date("2020-07-15T11:00:00.000Z"),
+    title: "Pirátský oběd - Chrudim",
+    description:
+      "Pravidelné setkání pirátů při středečním obědě. Nejen o politice a s chutí.",
+    link:
+      "https://www.google.com/calendar/event?eid=Mmw1Y2RwMTByYm80Y204cWxsaW1maWJmcTJfMjAyMDA3MTVUMTAwMDAwWiA3cjY3M3JsaDI1NW9mb3JodjNvZWIybDBnMEBn",
+    mapLink: "https://maps.google.com"
+  },
+  {
+    id: 16,
+    start: new Date("2020-07-20T19:00:00.000Z"),
+    startDateVerbose: "pondělí 20. července 2020",
+    startTimeVerbose: "21:00",
+    allDay: false,
+    end: new Date("2020-07-20T19:30:00.000Z"),
+    title: "Mumble - předsednictvo",
+    link:
+      "https://www.google.com/calendar/event?eid=YzVpM2FvaGc2MHAzY2I5aGM1aW1jYjlrNjBvbThiYjE2dGk2NGI5ajY4cjY0ZGhrNzVnamdjOWdjb18yMDIwMDcyMFQxOTAwMDBaIDdyNjczcmxoMjU1b2Zvcmh2M29lYjJsMGcwQGc"
+  },
+  {
+    id: 4,
+    start: new Date("2020-07-22T10:00:00.000Z"),
+    startDateVerbose: "středa 22. července 2020",
+    startTimeVerbose: "12:00",
+    allDay: false,
+    end: new Date("2020-07-22T11:00:00.000Z"),
+    title: "Pirátský oběd - Chrudim",
+    description:
+      "Pravidelné setkání pirátů při středečním obědě. Nejen o politice a s chutí.",
+    link:
+      "https://www.google.com/calendar/event?eid=Mmw1Y2RwMTByYm80Y204cWxsaW1maWJmcTJfMjAyMDA3MjJUMTAwMDAwWiA3cjY3M3JsaDI1NW9mb3JodjNvZWIybDBnMEBn"
+  },
+  {
+    id: 17,
+    start: new Date("2020-07-27T19:00:00.000Z"),
+    startDateVerbose: "pondělí 27. července 2020",
+    startTimeVerbose: "21:00",
+    allDay: false,
+    end: new Date("2020-07-27T19:30:00.000Z"),
+    title: "Mumble - předsednictvo",
+    link:
+      "https://www.google.com/calendar/event?eid=YzVpM2FvaGc2MHAzY2I5aGM1aW1jYjlrNjBvbThiYjE2dGk2NGI5ajY4cjY0ZGhrNzVnamdjOWdjb18yMDIwMDcyN1QxOTAwMDBaIDdyNjczcmxoMjU1b2Zvcmh2M29lYjJsMGcwQGc"
+  },
+  {
+    id: 5,
+    start: new Date("2020-07-29T10:00:00.000Z"),
+    startDateVerbose: "středa 29. července 2020",
+    startTimeVerbose: "12:00",
+    allDay: false,
+    end: new Date("2020-07-29T11:00:00.000Z"),
+    title: "Pirátský oběd - Chrudim",
+    description:
+      "Pravidelné setkání pirátů při středečním obědě. Nejen o politice a s chutí.",
+    link:
+      "https://www.google.com/calendar/event?eid=Mmw1Y2RwMTByYm80Y204cWxsaW1maWJmcTJfMjAyMDA3MjlUMTAwMDAwWiA3cjY3M3JsaDI1NW9mb3JodjNvZWIybDBnMEBn"
+  },
+  {
+    id: 18,
+    start: new Date("2020-08-03T19:00:00.000Z"),
+    startDateVerbose: "pondělí 3. srpna 2020",
+    startTimeVerbose: "21:00",
+    allDay: false,
+    end: new Date("2020-08-03T19:30:00.000Z"),
+    title: "Mumble - předsednictvo",
+    link:
+      "https://www.google.com/calendar/event?eid=YzVpM2FvaGc2MHAzY2I5aGM1aW1jYjlrNjBvbThiYjE2dGk2NGI5ajY4cjY0ZGhrNzVnamdjOWdjb18yMDIwMDgwM1QxOTAwMDBaIDdyNjczcmxoMjU1b2Zvcmh2M29lYjJsMGcwQGc"
+  },
+  {
+    id: 6,
+    start: new Date("2020-08-05T10:00:00.000Z"),
+    startDateVerbose: "středa 5. srpna 2020",
+    startTimeVerbose: "12:00",
+    allDay: false,
+    end: new Date("2020-08-05T11:00:00.000Z"),
+    title: "Pirátský oběd - Chrudim",
+    description:
+      "Pravidelné setkání pirátů při středečním obědě. Nejen o politice a s chutí.",
+    link:
+      "https://www.google.com/calendar/event?eid=Mmw1Y2RwMTByYm80Y204cWxsaW1maWJmcTJfMjAyMDA4MDVUMTAwMDAwWiA3cjY3M3JsaDI1NW9mb3JodjNvZWIybDBnMEBn"
+  }
+];
+
+const moreEvents = [
+  {
+    id: 19,
+    start: new Date("2020-08-10T19:00:00.000Z"),
+    startDateVerbose: "pondělí 10. srpna 2020",
+    startTimeVerbose: "21:00",
+    allDay: false,
+    end: new Date("2020-08-10T19:30:00.000Z"),
+    title: "Mumble - předsednictvo",
+    link:
+      "https://www.google.com/calendar/event?eid=YzVpM2FvaGc2MHAzY2I5aGM1aW1jYjlrNjBvbThiYjE2dGk2NGI5ajY4cjY0ZGhrNzVnamdjOWdjb18yMDIwMDgxMFQxOTAwMDBaIDdyNjczcmxoMjU1b2Zvcmh2M29lYjJsMGcwQGc"
+  },
+  {
+    id: 7,
+    start: new Date("2020-08-12T10:00:00.000Z"),
+    startDateVerbose: "středa 12. srpna 2020",
+    startTimeVerbose: "12:00",
+    allDay: false,
+    end: new Date("2020-08-12T11:00:00.000Z"),
+    title: "Pirátský oběd - Chrudim",
+    description:
+      "Pravidelné setkání pirátů při středečním obědě. Nejen o politice a s chutí.",
+    link:
+      "https://www.google.com/calendar/event?eid=Mmw1Y2RwMTByYm80Y204cWxsaW1maWJmcTJfMjAyMDA4MTJUMTAwMDAwWiA3cjY3M3JsaDI1NW9mb3JodjNvZWIybDBnMEBn"
+  }
+];
+
+export default {
+  data: () => ({
+    events: initialEvents,
+    hasMore: true,
+  }),
+  methods: {
+    onShowMore() {
+      this.$data.events = [...initialEvents, ...moreEvents];
+      this.$data.hasMore = false;
+    }
+  },
+  render() {
+    return this.$scopedSlots.default({
+      events: this.events,
+      hasMore: this.hasMore,
+      onShowMore: this.onShowMore,
+    });
+  }
+};
+</script>
diff --git a/source/js/components/calendar/GoogleProvider.vue b/source/js/components/calendar/GoogleProvider.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/source/js/components/calendar/Renderer.vue b/source/js/components/calendar/Renderer.vue
new file mode 100644
index 0000000000000000000000000000000000000000..395bf73c4c6e8374cf1a1085d08c927bd2166823
--- /dev/null
+++ b/source/js/components/calendar/Renderer.vue
@@ -0,0 +1,65 @@
+<template>
+  <div class="calendar grid grid-cols-4">
+    <div class="col-span-4 xl:col-span-1">
+      <aside class="banner bg-orange-300 text-white h-full">
+        <i class="ico--calendar banner__icon"></i>
+        <div class="banner__body">
+          <h1 class="head-alt-md banner__cta">{{ name }}</h1>
+          <button class="btn btn--white btn--fullwidth sm:btn--autowidth mt-8" v-if="onShowMore && hasMore" @click="onShowMore()">
+            <div class="btn__body">Zobrazit další</div>
+          </button>
+        </div>
+      </aside>
+    </div>
+    <div class="col-span-4 xl:col-span-3">
+      <div class="grid grid-cols-12 items-center calendar-table-row" v-for="event in events" v-bind:key="event.id">
+        <div class="col-span-2 text-orange-300 head-alt-md calendar-table-row__col"><span>{{ event.start | dateDay }}</span></div>
+        <div class="col-span-8 grid grid-cols-3 calendar-table-row__col" :class="{'calendar-table-row__col--norborder': !event.mapLink}">
+          <div class="col-span-3 md:col-span-1">
+            <strong class="block">{{ event.startDateVerbose }}</strong>
+            <p class="font-light text-sm mt-sm">{{ event.allDay ? "Celý den" : event.startTimeVerbose }}</p>
+          </div>
+          <div class="col-span-3 md:col-span-2 mt-4 md:mt-0">
+            <a v-if="event.link" v-bind:href="event.link" class="font-bold block" target="_blank" rel="noreferrer noopener">{{ event.title }}</a>
+            <strong v-if="!event.link" class="block">{{ event.title }}</strong>
+            <p class="font-light text-sm mt-sm" v-if="event.description">{{ event.description }}</p>
+          </div>
+        </div>
+        <div class="col-span-2 text-center font-light calendar-table-row__col">
+          <a :href="event.mapLink" v-if="event.mapLink" class="icon-link">
+            <i class="ico--location text-violet-300 mr-1" aria-hidden="true"></i>
+            <span>Mapa</span>
+          </a>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+  export default {
+    props: {
+      name: {
+        type: String,
+        default: "Kalendář"
+      },
+      events: {
+        type: Array,
+        required: true,
+      },
+      onShowMore: {
+        type: Function,
+        required: false,
+      },
+      hasMore: {
+        type: Boolean,
+        default: true,
+      }
+    },
+    filters: {
+      dateDay: (val) => {
+        return `${val.getDate()}.`;
+      }
+    }
+  };
+</script>
diff --git a/source/js/apps/footer/FooterCollapsible.vue b/source/js/components/footer/FooterCollapsible.vue
similarity index 100%
rename from source/js/apps/footer/FooterCollapsible.vue
rename to source/js/components/footer/FooterCollapsible.vue
diff --git a/source/js/apps/navbar/Navbar.vue b/source/js/components/navbar/Navbar.vue
similarity index 100%
rename from source/js/apps/navbar/Navbar.vue
rename to source/js/components/navbar/Navbar.vue
diff --git a/source/js/apps/navbar/NavbarSubitem.vue b/source/js/components/navbar/NavbarSubitem.vue
similarity index 100%
rename from source/js/apps/navbar/NavbarSubitem.vue
rename to source/js/components/navbar/NavbarSubitem.vue
diff --git a/source/js/main.js b/source/js/main.js
index 5cdb96b91493e436004d800fadd03e6b88b442f0..0d4dd8ba286c26a5ba716a3f63661d3dec16b40a 100644
--- a/source/js/main.js
+++ b/source/js/main.js
@@ -1,7 +1,38 @@
 import Vue from "vue";
 
 import { forEachNode } from "./utils";
-import Apps from "./apps";
+
+import Renderer from "./components/calendar/Renderer";
+import DummyProvider from "./components/calendar/DummyProvider";
+import RegionMap from "./components/RegionMap";
+import ViewProvider from "./components/ViewProvider";
+import Navbar from "./components/navbar/Navbar";
+import FooterCollapsible from "./components/footer/FooterCollapsible";
+import FlipClock from "./components/FlipClock";
+
+
+Vue.component("ui-calendar-renderer", Renderer);
+Vue.component("ui-calendar-dummy-provider", DummyProvider);
+Vue.component("ui-region-map", RegionMap);
+Vue.component("ui-view-provider", ViewProvider);
+Vue.component("ui-navbar", Navbar);
+Vue.component("ui-footer-collapsible", FooterCollapsible);
+Vue.component("ui-flip-clock", FlipClock);
+
+
+import UiApp from "./components/UiApp.vue";
+
+
+const appFactory = (el, attrs) => {
+  // Bootstrap Vue.js.
+  new Vue({
+    el,
+    components: {
+      UiApp
+    }
+  });
+};
+
 
 /**
  * Bootstrap Vue.js application at given Element instance.
@@ -14,20 +45,7 @@ import Apps from "./apps";
  */
 function renderVueAppElement(el) {
   const attrs = Object.assign({}, el.dataset);
-
-  if (! attrs.app) {
-    console.warn(el, 'Cannot bootstrap: missing data-app');
-    return;
-  }
-
-  const app = Apps[attrs.app];
-
-  if (! app) {
-    console.warn(el, `Cannot bootstrap: unknown app ${attrs.app}`);
-    return;
-  }
-
-  return app(el, attrs);
+  return appFactory(el, attrs);
 }
 
 
diff --git a/tailwind-plugins/buttons.js b/tailwind-plugins/buttons.js
index 8a57b791ecfe14b845ef05bc8dfa3277e9ae4cfd..1761f09102cc2b1ca986f7309635ac13ce44d3f8 100644
--- a/tailwind-plugins/buttons.js
+++ b/tailwind-plugins/buttons.js
@@ -11,56 +11,7 @@ function defaultOptions() {
     display: 'inline-block',
     fontWeight: '400',
     maxWidth: '20rem',
-    colors: {
-      // white: {
-      //   background: defaultConfig.colors['white'],
-      //   text: defaultConfig.colors['black'],
-      // },
-      // black: {
-      //   background: defaultConfig.colors['black'],
-      //   text: defaultConfig.colors['white'],
-      // },
-      // grey: {
-      //   background: defaultConfig.colors['grey'],
-      //   text: defaultConfig.colors['black'],
-      // },
-      // red: {
-      //   background: defaultConfig.colors['red'],
-      //   text: defaultConfig.colors['white'],
-      // },
-      // orange: {
-      //   background: defaultConfig.colors['orange'],
-      //   text: defaultConfig.colors['white'],
-      // },
-      // yellow: {
-      //   background: defaultConfig.colors['yellow'],
-      //   text: defaultConfig.colors['yellow-darkest'],
-      // },
-      // green: {
-      //   background: defaultConfig.colors['green'],
-      //   text: defaultConfig.colors['white'],
-      // },
-      // teal: {
-      //   background: defaultConfig.colors['teal'],
-      //   text: defaultConfig.colors['white'],
-      // },
-      // blue: {
-      //   background: defaultConfig.colors['blue'],
-      //   text: defaultConfig.colors['white'],
-      // },
-      // indigo: {
-      //   background: defaultConfig.colors['indigo'],
-      //   text: defaultConfig.colors['white'],
-      // },
-      // purple: {
-      //   background: defaultConfig.colors['purple'],
-      //   text: defaultConfig.colors['white'],
-      // },
-      // pink: {
-      //   background: defaultConfig.colors['pink'],
-      //   text: defaultConfig.colors['white'],
-      // },
-    },
+    colors: {},
   }
 }