Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2015 Deutsche Telekom, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,364 @@
html {
font-size: 10px;
background-color: #F4F4F4;
}
[role="toolbar"] {
position: static;
}
ul {
font-size: 0.9em;
}
.gaia-icon::before {
font-style: normal;
position: relative;
}
.icon-settings::before {
color: white;
top: 2px;
left: 1px;
}
.note {
font-size: 0.7em;
padding-left: 15px;
padding-bottom: 5px;
background-color: rgb(248, 246, 246);
display: none;
}
#main article > header {
background-color: rgb(248, 246, 246);
color: #F97C17;
height: 30px;
line-height: 30px;
text-align: center;
text-transform: uppercase;
border-bottom: 1px white solid;
border-top: 1px white solid;
/* box-shadow: 0px 1px 5px 0px rgba(50, 50, 50, 0.1);*/
font-size: 12px;
box-shadow: 0px 1px 1px 0px rgba(33, 33, 33, 0.1);
}
.bottom-article > header {
z-index: 3;
position: relative;
}
.hide, .listview-hide {
display: none;
}
#settings p {
margin: 10px;
}
.error {
color: red;
}
section[role="region"] > header:first-child h1:nth-child(2) {
margin-left: 50px;
}
#hci-notification {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 300;
transition: all 0.6s ease;
transform: translateY(100%);
}
#hci-notification img {
width: 240px;
box-shadow: 0px 1px 4px 1px #7B7B7B;
border-radius: 8px;
}
#hci-notification.show {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 300;
transition: all 0.6s ease;
transform: translateY(0);
}
article.notification {
color: #9C9C9C;
font-size: 0.7em;
font-style: italic;
opacity: 0.95;
background: black;
padding-bottom: 10px;
}
.hci-card {
position: absolute;
width: 100%;
height: 100%;
color: white;
z-index: 1;
}
.hci-container {
text-align: center;
border-radius: 10px;
padding: 0px 20px;
color: rgb(77, 78, 83);
background: transparent linear-gradient(to bottom, rgb(234, 239, 242), rgb(212, 221, 228));
}
.hci-container p {
line-height: 3em;
font-weight: 300;
font-size: 0.8em;
}
@keyframes show-card {
0% { z-index: 2; transform: rotate(0deg); top: 0px; left: 0px; width: 240px;}
50% { z-index: 10; transform: rotate(0deg); top: -30px; left: -2px; width: 244px;}
100% { z-index: 10; transform: rotate(0deg); top: -8px; left: 0px; width: 240px;}
}
@keyframes hide-card {
0% { z-index: 10; transform: rotate(0deg); top: -8px; left: 0px; width: 240px;}
50% { z-index: 10; transform: rotate(0deg); top: -30px; left: -2px; width: 244px;}
100% { z-index: 2; transform: rotate(0deg); top: 0px; left: 0px; width: 240px;}
}
ul.swipeable > li.deselected > img {
animation-name: hide-card;
animation-duration: 0.5s;
z-index: 2;
position: relative;
top: 0px;
}
ul.swipeable > li.selected > img {
z-index: 10;
transform: rotate(0deg);
position: relative;
top: -8px;
animation-name: show-card;
animation-duration: 0.5s;
}
ul.details > li {
display: block;
padding: 15px 10px;
}
ul.details > li > p.label {
font-size: 1em;
}
.details span.date {
display: inline-block;
width: 67%;
}
li.details-more {
text-align: center;
vertical-align: bottom;
margin-right: 15px;
}
li.details-more > .details-note {
font-style: italic;
font-size: 0.8em;
color: rgb(133, 133, 133);
vertical-align: bottom;
}
/*.details-oneline p {
display: inline-block;
}*/
.active > header {
z-index: 3;
}
.slider {
background-color: rgb(248, 246, 246);
/* transform: translate3d(0, -131px, 0);
box-shadow: inset 0px 1px 5px 0px rgba(50, 50, 50, 0.1);
box-shadow: 0px 5px 5px 0px rgba(33, 33, 33, 0.1) inset; */
padding-top: 1rem;
height: 25rem;
overflow: hidden;
}
.bottom-slide .slider {
box-shadow: 0px 2px 2px 0px rgba(33, 33, 33, 0.1) inset;
}
.bottom-slide > header {
position: relative;
}
.top-slide {
margin-top: -26rem;
transition: transform 0.75s, margin-top 0.75s ease;
}
.details-visible .top-slide {
margin-top: 0px;
overflow: auto;
transform: translate3d(0,0,0)
}
.bottom-slide {
z-index: 3;
position: relative;
top: -10px;
transition: transform 0.75s, top 0.75s ease;
}
.bottom-slide.details-visible {
top: -253px;
transform: translate3d(0,0,0)
}
section[role="region"] > header:first-child .t-logo {
background: url(../img/icons/t_logo_white.png) center center / 5rem auto no-repeat;
width: 5rem;
}
.pocket, .listview-element-pocket {
width: 260px;
height: 40px;
border-radius: 12px;
position: relative;
z-index: 5;
top: 150px;
left: 0px;
background: linear-gradient(to bottom, rgb(200, 200, 200) 0%, #F4F4F4 77%, #F4F4F4 100%) repeat scroll 0% 0% transparent;
border-top: 1px solid #808080;
opacity: 0.9;
}
/*.nonpayment*/ .details li {
min-height: 0px
}
/*.nonpayment*/ .details p {
display: inline;
}
/*.nonpayment*/ .details p.val {
float: right;
margin-right: 15px;
}
.details .price {
font-family: "Fira Mono";
font-size: 13px;
width: 21%;
display: inline-block;
text-align: right;
}
.details .currency {
font-family: "Fira Mono";
font-size: 13px;
}
.sew, .listview-element-sew {
width: 92%;
height: 10px;
margin: 2px 0px 0px 5px;
border-radius: 12px;
padding-right: 10px;
border-top: 1px dashed #8C8C8C;
}
ul.transaction-list > li.transaction {
padding: 4px 0px 1px 10px;
color: rgb(133, 133, 133);
}
ul.transaction-list > li.transaction:after {
border: 0px none;
}
ul.details > li p {
font-size: 1em;
margin-left: 10px;
line-height: 1em;
}
ul.details > li p.value {
float: right;
margin-right: 10px;
font-size: 0.9em;
}
.error-view {
text-align: center;
padding: 10px;
margin-top: -2em;
}
.error-view h2.error {
font-weight: normal;
font-size: 1.2em;
}
.error-view button {
margin-bottom: 1em;
}
.error-img {
width: 5em;
height: 5em;
background-image: url(../img/errors/error.png);
background-size: contain;
background-repeat: no-repeat;
background-position: center center;
display: inline-block;
}
.error-api {
background-image: url(../img/errors/api-error.png);
}
.error-no-sim {
background-image: url(../img/errors/no-sim.png);
}
.error-sim {
background-image: url(../img/errors/sim-error.png);
}
.footer {
position: absolute;
left: 0px;
right: 0px;
bottom: 0px;
margin: 10px 10px 0px 10px;
}
@media (min-width: 360px) {
.bottom-slide {
top: -1px;
}
.bottom-slide.details-visible {
top: -26rem;
}
ul.details > li.details-more {
padding-right: 20px;
padding-left: 20px;
}
}

View file

@ -0,0 +1,131 @@
section#pin {
background-color: #F4F4F4;
}
.pin-unblocked {
font-weight: 300;
color: #333;
}
.pin-view h2 {
font-size: 1.2em;
font-weight: 300;
text-align: center;
}
.pin-view:not(hide) {
z-index: 200;
}
.pin-top {
/*font-style: italic;*/
height: 7.3em;
}
.pin-input {
text-align: center;
margin: 35px 0px 30px;
}
.pin-input > div {
border-radius: 50%;
display: inline-block;
width: 14px;
height: 14px;
border: 1.5px solid #ccc;
vertical-align: baseline;
margin: 0px 5px;
}
.pin-input > div.pin-digit-entered {
background: linear-gradient(to bottom, #f4f4f4 0%,#333333 100%);
border: 1.5px solid #333;
}
.pin-keypad {
display: table;
text-align: center;
border-collapse: collapse;
margin: 0px 0px 25px 0px;
}
.pin-keypad-row {
border-width: 0px 0px 1px 0px;
border-style: solid;
border-color: transparent;
/*border-image: linear-gradient(45deg, #f4f4f4, #ccc, #f4f4f4) 1;*/
border-image: linear-gradient(to right, #f4f4f4 0%,#cccccc 30%,#cccccc 70%,#f4f4f4 100%) 1;
}
.pin-keypad-row:last-child {
border: none;
}
.pin-key {
display: table-cell;
width: 2.7em;
height: 2.7em;
vertical-align: middle;
border-right: 1px solid #ccc;
font-size: 1.7em;
cursor: pointer;
}
.pin-key.clear-key::after {
content: "clear";
font-size: 0.5em;
vertical-align: 35%;
}
.pin-key:active {
color: #F97C17;
/*background-color: #ccc;*/
}
.pin-key.gaia-icon::before {
position: relative;
font-size: 1em;
}
.pin-key.icon-clearcancel-cross::before {
font-size: 1.7em;
}
.pin-keypad-row:first-child .pin-key {
background: linear-gradient(to bottom, #f4f4f4, #ccc);
background-origin: border-box;
border-right: 1px solid transparent;
box-shadow: inset -999px 0 0 #f4f4f4;
}
.pin-keypad-row:last-child .pin-key {
background: linear-gradient(to top, #f4f4f4, #ccc);
background-origin: border-box;
border-right: 1px solid transparent;
box-shadow: inset -999px 0 0 #f4f4f4;
}
.pin-keypad-row .pin-key:last-child {
border: none;
}
.pin-warning {
margin-bottom: -39px;
position: relative;
top: -13px;
font-size: 0.85em;
text-align: center;
}
.pin-enable-warning {
margin-top: 1em;
}
.error-pin-enable {
background-image: url(../img/errors/pin-enable-fail.png);
}
.error-locked {
background-image: url(../img/errors/locked.png);
}

View file

@ -0,0 +1,172 @@
#settings {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
opacity: 1;
transition: all 0.6s ease;
background-color: #F4F4F4;
transform: translateY(100%);
}
#settings.edit {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
opacity: 1;
transition: all 0.6s ease;
transform: translateY(0);
}
.settings-subpage {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 101;
opacity: 1;
background-color: #F4F4F4;
transition: transform 0.6s ease;
transform: translateX(100%);
}
.settings-subpage.slide-in {
transition: transform 0.6s ease;
transform: translateX(0);
}
.settings-disabled {
pointer-events: none;
color: #ccc;
}
.settings-disabled .gaia-icon::before {
color: #ccc;
}
.settings-disclaimer {
font-size: 0.6em;
padding: 0px 15px 10px 15px;
line-height: 1.5em;
}
.settings-forward div {
width: 100%;
margin-left: 15px;
}
.settings-forward .gaia-icon {
text-align: right;
color: #868692;
}
.settings-forward .gaia-icon::before {
position: relative;
font-size: 1.5rem;
}
#settings-status {
opacity: 0;
transition: opacity 600ms ease 0s;
}
#settings-status.visible {
opacity: 1;
transition: opacity 1000ms ease 0s;
}
#settings-status p{
text-align: center;
}
#settings-about section > div {
text-align: center;
border-bottom: 1px solid #E7E7E7;
padding: 10px;
margin: 0px 15px;
}
#settings-about div:last-child {
border: none;
}
#settings-about div:first-child {
padding-top: 0px;
}
.about-logo img {
max-width: 9em;
}
.about-logo p {
line-height: 1.3em;
}
.about-logo p:first-child {
font-size: 1.1em;
}
.about-logo p:nth-child(2) {
font-size: 1.5em;
}
.about-logo p:nth-child(4) {
font-size: 1em;
}
.about-authors p, .about-contributors p {
font-size: 0.8em;
color: #a6a6a6;
line-height: 1.5em;
}
.about-authors strong, .about-contributors strong {
color: #333;
font-weight: normal;
}
.about-authors p {
text-align: left;
}
.about-authors strong {
float: right;
}
.about-contributors div {
margin-bottom: 0.5em;
}
.about-contributors h3 {
font-size: 0.8em;
margin-bottom: 2px;
margin-top: 1px;
font-weight: 500;
}
.about-contributors div p:nth-child(2) {
line-height: 1em;
margin: 5px 0px;
}
.about-contributors div p:nth-child(3) {
line-height: 1em;
font-size: 0.6em;
color: #333;
}
/* ugly hacks to override header.css, TODO migrate to proper gaia header */
section[role="region"].settings-subpage > header:first-child h1:nth-child(2) {
margin: -1px;
}
section.skin-organic.settings-subpage[role="region"] > header:first-child .gaia-icon {
color: #868692;
background: none;
}

View file

@ -0,0 +1,70 @@
ul.swipeable {
white-space: nowrap;
overflow: hidden;
font-size: 0px;
}
ul.swipeable > li {
display: inline-block;
width: 260px;
height: 206px;
overflow: hidden;
}
ul.swipeable > li > img, .listview-element-img {
width: 240px;
padding: 20px 10px 25px;
margin-top: -35px;
position: relative;
}
ul.swipeable > li:first-child {
margin-left: 3rem;
}
.swipeable-container {
z-index: 2;
background-color: #f4f4f4;
border-bottom: 1px solid #FBFBFB;
box-shadow: 0px 2px 2px 0px rgba(33, 33, 33, 0.1);
}
.swipeable-progress {
text-align: center;
height: 1.6rem;
position: relative;
z-index: 1;
}
.swipeable-progress div{
border-radius: 50%;
display: inline-block;
width: 6px;
height: 6px;
border: 2px solid #aeaeae;
vertical-align: baseline;
margin: 0px 2px;
}
.swipeable-progress div.swipeable-progress-selected {
background: #aeaeae;
}
.swipeable-progress div.swipeable-progress-active {
/* background-color: #e20074; */
border: 2px solid #F97C17;
}
.swipeable-progress div.swipeable-progress-active.swipeable-progress-selected {
background: #F97C17;
}
@media (min-width: 360px) {
ul.swipeable > li:first-child {
margin-left: 5rem;
}
.swipeable-container {
padding-top: 0.5rem;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View file

@ -0,0 +1,361 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Mobile Wallet</title>
<meta name="theme-color" content="#ec7616">
<meta name="description" content="New wallet version targeted for FFOS 2.2">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<link href="shared/style/layout.css" rel="stylesheet" type="text/css">
<link href="shared/style/headers.css" rel="stylesheet" type="text/css">
<link href="shared/style/buttons.css" rel="stylesheet" type="text/css">
<link href="shared/style/input_areas.css" rel="stylesheet" type="text/css">
<link href="shared/style/status.css" rel="stylesheet" type="text/css">
<link href="shared/style/icons.css" rel="stylesheet" type="text/css">
<link href="css/app.css" rel="stylesheet" type="text/css">
<link href="css/swipeable.css" rel="stylesheet" type="text/css">
<link href="css/settings.css" rel="stylesheet" type="text/css">
<link href="css/pin.css" rel="stylesheet" type="text/css">
<script src="/shared/js/component_utils.js" defer></script>
<script src="/shared/elements/gaia_switch/script.js" defer></script>
<script type="text/javascript" src="shared/js/gesture_detector.js" defer></script>
<script type="text/javascript" src="js/error/error_consts.js" defer></script>
<script type="text/javascript" src="js/error/error_view.js" defer></script>
<script type="text/javascript" src="js/common/utils.js" defer></script>
<script type="text/javascript" src="js/common/details_view_mixin.js" defer></script>
<script type="text/javascript" src="js/common/list_view_mixin.js" defer></script>
<script type="text/javascript" src="js/common/card_swipe_mixin.js" defer></script>
<script type="text/javascript" src="js/common/swipeable.js" defer></script>
<script type="text/javascript" src="js/secure-element/se_utils.js" defer></script>
<script type="text/javascript" src="js/secure-element/apdu_helpers.js" defer></script>
<script type="text/javascript" src="js/settings/default_settings.js" defer></script>
<script type="text/javascript" src="js/secure-element/sim_access_manager.js" defer></script>
<script type="text/javascript" src="js/settings/settings_sub_view.js" defer></script>
<script type="text/javascript" src="js/settings/config_sub_view.js" defer></script>
<script type="text/javascript" src="js/settings/settings_view.js" defer></script>
<script type="text/javascript" src="js/settings/settings_controller.js" defer></script>
<script type="text/javascript" src="js/pin/pin_controller.js" defer></script>
<script type="text/javascript" src="js/pin/pin_view.js" defer></script>
<script type="text/javascript" src="js/applets/applet_model.js" defer></script>
<script type="text/javascript" src="js/applets/applet_details_view.js" defer></script>
<script type="text/javascript" src="js/applets/applet_list_view.js" defer></script>
<script type="text/javascript" src="js/applets/applet_list_controller.js" defer></script>
<script type="text/javascript" src="js/applets/hci_view.js" defer></script>
<script type="text/javascript" src="js/ext_data.js" defer></script>
<script type="text/javascript" src="js/app_view.js" defer></script>
<script type="text/javascript" src="js/app.js" defer></script>
</head>
<body>
<section role="region" id="global-error" class="vbox fit hide">
<header>
<h1 class="title">Mobile Wallet</h1>
</header>
<section class="vbox fit hbox middle center">
<article id="unknown-error" class="error-view">
<div class="error-img"></div>
<h2 class="error">Unknown problem occured</h2>
<p>
There's something wrong, but we're not sure what that is. Please contact your wallet provider for assistance.
</p>
</article>
<article id="no-sim" class="error-view hide">
<div class="error-img error-no-sim"></div>
<h2 class="error">Your SIM card is missing</h2>
<p>
We're not able to detect SIM card in your phone.
Please make sure it's inserted properly.
If the problem persists, please contact wallet provider for assistance.
</p>
</article>
<article id="no-api" class="error-view hide">
<div class="error-img error-api"></div>
<h2 class="error">SE API not detected</h2>
<p>
Most probably your phone has some compatibility issues or not up-to-date software.
Please make sure you're running the latest available version of Firefox OS.
If the problem persists, please contact your wallet provider for assistance.
</p>
</article>
<article id="sim-error" class="error-view hide">
<div class="error-img error-sim"></div>
<h2 class="error">SIM problem detected</h2>
<p>
It seems your SIM card has some technical issues. Please confirm that applet AID values in Settings are vaild and application is authorized to access them.
If the problem persists please contact your wallet provider for assistance.
</p>
<section class="footer">
<button class="error-settings-btn">Settings</button>
</section>
</article>
</section>
</section>
<section role="region" id="pin" class="vbox fit pin-view hide">
<header>
<h1 class="title">Mobile Wallet</h1>
</header>
<section id="pin-template" class="vbox fit hbox middle center">
<article class="pin-unblocked vbox middle center">
<div class="pin-top pin-entry">
<h2 class="pin-entry-text">Please enter your PIN</h2>
<div class="pin-warning error hide">
<p><span class="pin-attempts"></span></p>
</div>
<div class="pin-input">
</div>
</div>
<div class="pin-top hide vbox middle pin-incorrect">
<h2 class="pin-entry-error-text error">Incorrect PIN</h2>
</div>
<div class="pin-keypad">
<div class="pin-keypad-row">
<div class="pin-key" data-value="1">1</div>
<div class="pin-key" data-value="2">2</div>
<div class="pin-key" data-value="3">3</div>
</div>
<div class="pin-keypad-row">
<div class="pin-key" data-value="4">4</div>
<div class="pin-key" data-value="5">5</div>
<div class="pin-key" data-value="6">6</div>
</div>
<div class="pin-keypad-row">
<div class="pin-key" data-value="7">7</div>
<div class="pin-key" data-value="8">8</div>
<div class="pin-key" data-value="9">9</div>
</div>
<div class="pin-keypad-row">
<div class="pin-key clear-key" data-value="clear"></div>
<div class="pin-key" data-value="0">0</div>
<div class="pin-key gaia-icon icon-dialer-delete" data-value="backspace"></div>
</div>
</div>
</article>
<article class="pin-blocked error-view hide">
<div class="error-img error-locked"></div>
<h2 class="error">Your wallet was locked</h2>
<p>
We're sorry, but it looks like you entered incorrect PIN too many times.
Please unlock your wallet by entering PUK or contact your wallet provider for assistance.
</p>
<section class="footer">
<button class="pin-unblock-btn">Enter PUK</button>
</section>
</article>
<article class="puk-blocked error-view hide">
<div class="error-img error-locked"></div>
<h2 class="error">Your wallet was locked</h2>
<p>
We're sorry, but it looks like you entered incorrect PUK too many times.
Please contact your wallet provider for assistance.
</p>
</article>
<article class="pin-enable-error error-view hide">
<div class="error-img error-pin-enable"></div>
<h2 class="error">Enabling PIN lock failed</h2>
<p>
We're not able to enable PIN lock in your wallet. Most probably you're
entering incorrect PIN. If you haven't used wallet PIN lock before,
you might want to try the default value, which is
<span class="pin-default-val">0000</span>. If the problem persists, please
contact your wallet service provider.
<div class="pin-enable-warning error hide">
<p><span class="pin-enable-attempt">0 attempts</span> remaining</p>
</div>
</p>
</article>
</section>
</section>
<section role="region" id="main" class="vbox fit hide">
<header>
<menu type="toolbar">
<button class="gaia-icon icon-settings" id="settings-edit"></button>
</menu>
<h1 class="title">Mobile Wallet</h1>
</header>
<section id="cards-view" class="fit">
<article id="payments-view" class="vbox middle active">
<header>Your Payment cards</header>
<div class="swipeable-container">
<div class="swipeable-progress"></div>
<ul class="swipeable">
<li>
<div class="pocket">
<div class="sew"></div>
</div>
<img src="img/cards/no-cards-payment.png">
</li>
</ul>
</div>
<div id="payments-view-details" class="slider top-slide gaia-list">
<ul class="details">
<li><p class="aid">Card number</p><p class="val number"></p></li>
<li><p class="date">Valid thru</p><p class="val issued"></p></li>
<li class="transactions">
<p class="transaction"><span class="date"></span> <span class="price"></span><span class="currency"></span></p>
</li>
</ul>
</div>
</article>
<article id="nonpayments-view" class="bottom-slide">
<header>Your Other Cards</header>
<div class="swipeable-container" id="s2">
<div id="s2-progress" class="swipeable-progress"></div>
<ul class="swipeable">
<li>
<div class="pocket">
<div class="sew"></div>
</div>
<img src="img/cards/no-cards-other.png">
</li>
</ul>
</div>
<div id="nonpayments-view-details" class="slider gaia-list">
<ul class="nonpayment details"></ul>
</div>
</article>
</section>
</section>
<section role="region" id="settings" class="vbox fit scroll skin-organic">
<header>
<menu type="toolbar">
<button id="settings-done">Done</button>
</menu>
<h1 class="title ">Settings</h1>
</header>
<section class="gaia-list fit scroll">
<article>
<section>
<ul>
<li>
<gaia-switch id="fast-pay">
<label>Fast Pay Mode</label>
</label>
</li>
<li>
<gaia-switch id="pin-checkbox">
<label>Enable PIN</label>
</gaia-switch>
</li>
<li id="pin-change-btn" class="settings-disabled settings-forward">
<div>Change PIN</div>
<aside class="gaia-icon icon-forward"></aside>
</li>
<li id="about-btn" class="settings-forward">
<div>About</div>
<aside class="gaia-icon icon-forward"></aside>
</li>
<li id="adv-settings-btn" class="settings-forward">
<div>Advanced Settings</div>
<aside class="gaia-icon icon-forward"></aside>
</li>
</ul>
</section>
</article>
</section>
<section role="status" id="settings-status">
<p></p>
</section>
</section>
<section role="region" id="settings-about" class="settings-subpage vbox fit skin-organic">
<header>
<button class="gaia-icon icon-back"></button>
<h1 class="title ">About</h1>
</header>
<section class="gaia-list fit subview-content">
<div class="about-logo">
<p>Open Source</p>
<p>Mobile Wallet</p>
<img src="img/t-logo.png">
<p>by Deutsche Telekom</p>
</div>
<div class="about-authors">
<p>Code <strong>Krzysztof Mioduszewski</strong></p>
<p>Design <strong>Bartosz Ziółkowski</strong></p>
</div>
<div class="about-contributors">
<h3>Contributors</h3>
<div>
<p>Secure Element API Development</p>
<p>
<strong>
Garner Lee, Krzysztof Mioduszewski, Siddartha Pothapragada, Arno Puder
</strong>
</p>
<p>(Deutsche Telekom Silicon Valley Innovation Center)</p>
</div>
<div>
<p>SIM Integration Support</p>
<p>
<strong>
Andrzej Baraniak, Łukasz Leszek
</strong>
</p>
<p>(T-Mobile Polska)</p>
</div>
<div>
<p>Mobile App Consultancy</p>
<p>
<strong>
Adam Paziewski, Radosław Rzepkowski
</strong>
</p>
<p>(T-Mobile Polska)</p>
</div>
</div>
</section>
</section>
<section role="region" id="settings-pin" class="settings-subpage vbox fit scroll skin-organic pin-view">
<header>
<button class="gaia-icon icon-back"></button>
<h1 class="title ">PIN Toggle</h1>
</header>
<section class="gaia-list fit scroll subview-content">
</section>
</section>
<section role="region" id="settings-configuration" class="settings-subpage vbox fit scroll skin-organic">
<header>
<button class="gaia-icon icon-back"></button>
<menu type="toolbar">
<button id="settings-configuration-done">Done</button>
</menu>
<h1 class="title ">Advanced Settings</h1>
</header>
<section class="gaia-list fit scroll subview-content">
<section>
<p class="settings-disclaimer">
This menu lets you configure SE-related parameters.
Changing these settings can cause the app to stop working.
To restore original settings, please reinstall the app.
</p>
<header>Contactless Registry Service AID</header>
<p>
<input id="crs-aid" placeholder="CRS AID" required="true" type="text" pattern="[a-fA-F0-9]{10,32}">
</p>
<header>Universal Integrated Circuit Card AID</header>
<p>
<input id="uicc-aid" placeholder="UICC AID" required="true" type="text" pattern="[a-fA-F0-9]{10,32}">
</p>
<header>PIN P2</header>
<p>
<input id="pinP2" placeholder="PIN P2" required="true" type="text" pattern="[a-fA-F0-9]{2}">
</p>
<header>Default PIN</header>
<p>
<input id="default-pin" placeholder="Default PIN" required="true" type="text" pattern="[0-9]{4}">
</p>
</section>
</section>
</section>
<section role="region" id="hci-notification" class="vbox fit">
<div class="hci-card vbox hbox middle center">
<div class="hci-container">
<p>You paid using:</p>
<img id="hci-image" src="img/cards/payment_blue.png">
<p id="hci-number"></p>
</div>
</div>
<article class="notification vbox fit hbox bottom center">
<p class="bottom-note">Tap to dismiss</p>
</article>
</section>
</body>
</html>

View file

@ -0,0 +1,127 @@
/* globals AppView, ERRORS, SIMAccess, SettingsController, PinController,
AppletListView, AppletListController, HCIView, SEUtils, PAYMENT_IMG_SRC,
NONPAYMENT_IMG_SRC, PAYMENT_APPLET_FILTER, NONPAYMENT_APPLET_FILTER,
SIM_UPDATE, SIM_REFRESH */
window.addEventListener('DOMContentLoaded', function() {
'use strict';
var log = (msg) => console.log('[main-ctrl] ' + msg);
var appState = { initialised: false, sentToBackground: false };
var appView = new AppView('main', 'global-error');
if(!window.navigator.seManager) {
log('Secure Element API not available. Exiting.');
appView.showErrorView(ERRORS.GLOBAL.NO_API);
return;
}
// components and views instantiation
var pin = new PinController(SIMAccess, 'pin-ctrl', 'pin');
var settingsPin = new PinController(SIMAccess, 'settings-pin-ctrl',
'settings-pin-view', 'pin-template');
var settings = new SettingsController('settings-ctrl', 'settings',
settingsPin);
var paymentsView = new AppletListView('payments-view', PAYMENT_IMG_SRC);
var hciView = new HCIView('hci-notification', PAYMENT_IMG_SRC);
var payments = new AppletListController(SIMAccess,'payments-ctrl',
paymentsView, PAYMENT_APPLET_FILTER);
var nonPaymentsView = new AppletListView('nonpayments-view',
NONPAYMENT_IMG_SRC);
var nonPayments =
new AppletListController(SIMAccess, 'nonpayments-ctrl', nonPaymentsView,
NONPAYMENT_APPLET_FILTER, true);
// updating SIMAccess with settings read from localStorage
SIMAccess.crsAid = settings.crsAid;
SIMAccess.uiccAid = settings.uiccAid;
SIMAccess.pinP2 = settings.pinP2;
// pin verification
var performPinCheck = function performPinCheck() {
if(settings.pinEnabled && !pin.pinCheckInProgress) {
appView.hideMainView();
return pin.verifyPin().then((verified) => {
if(!verified) {
return Promise.reject('Pin verification failed');
}
appView.showMainView();
});
}
return Promise.resolve();
};
// app starting point
var initApp = function intitApp() {
appView.showMainView();
SIMAccess.start()
.then(performPinCheck)
.then(() => payments.init())
.then(() => nonPayments.init())
.then(() => { appState.initialised = true; })
.catch((error) => {
log('Init promise chain broken. Reason: ' + error);
appView.showErrorView(error);
SIMAccess.stop();
});
};
// global events, system-messages handlers
document.addEventListener('visibilitychange', () => {
log('Visibility change');
if(!document.hidden && appState.initialised) {
performPinCheck().catch((e) => appView.showErrorView(e));
}
appState.sentToBackground = document.hidden;
});
window.navigator.mozSetMessageHandler('nfc-hci-event-transaction', (msg) => {
var shouldClose = !appState.initialised || appState.sentToBackground;
var aid = SEUtils.byteToHexString(msg.aid);
log('Got HCI EVT_TRANSACTION from AID ' + aid);
// SIM card is not accessible for the first 1.5s after HCI
setTimeout(() => payments.handleHCIEvt(msg, settings.fastpayEnabled), 2000);
hciView.show(aid, shouldClose);
});
// coponent specific event handlers
appView.addListener({ onEvent: (id, data) => {
if(data.action === 'open-settings') {
settings.show();
appView.hideErrorView();
}
}});
settings.addListener({ onEvent: (id, data) => {
log('Settings event: ' + JSON.stringify(data));
if(data.error) {
appView.showErrorView(data.error);
return;
}
if(data.action === 'settings-updated') {
var refreshNeeded = false;
Object.keys(data.changes).forEach(key => {
refreshNeeded = refreshNeeded || SIM_REFRESH.indexOf(key) !== -1;
if(SIM_UPDATE.indexOf(key) !== -1) {
SIMAccess[key] = data.changes[key];
}
});
if(refreshNeeded) {
initApp();
}
}
}});
log('Init access, getting applets from CRS');
initApp();
});

View file

@ -0,0 +1,53 @@
'use strict';
/* globals ErrorView, addMixin, ObserverSubjectMixin */
/* exported AppView */
(function(exports) {
var AppView = function(mainViewId, errorViewId) {
this._mainEl = document.querySelector('#' + mainViewId);
this._errorView = new ErrorView(errorViewId);
this._errorView.addListener(this);
var settingsBtn = this._mainEl.querySelector('#settings-edit');
settingsBtn.addEventListener('click', () => this._settingsClicked());
addMixin(this, ObserverSubjectMixin);
};
AppView.prototype = {
_mainEl: null,
_errorView: null,
showMainView: function showMainView() {
this._mainEl.classList.remove('hide');
this._errorView.hide();
},
hideMainView: function hideMainView() {
this._mainEl.classList.add('hide');
},
showErrorView: function showErrorView(error) {
this._errorView.handleError(error);
this.hideMainView();
},
hideErrorView: function hideErrorView() {
this._errorView.hide();
},
onEvent: function onEvent(id, data) {
if(data.action === 'open-settings') {
this._settingsClicked();
}
},
_settingsClicked: function _settingsClicked() {
this._notify({ action: 'open-settings' });
},
},
exports.AppView = AppView;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,32 @@
'use strict';
/* globals addMixin, DetailsViewMixin, DebugMixin */
/* exported AppletDetailsView */
(function(exports) {
var AppletDetailsView = function(id, applet) {
addMixin(this, DetailsViewMixin);
addMixin(this, DebugMixin);
this.initDetailsView(id, applet);
};
AppletDetailsView.prototype = {
_template:
'<ul class="details">' +
'<li id="aid"><p class="label">AID</p><p class="value"></p></li>' +
'<li class="details-more">' +
'<p class="details-note">' +
'More details read from the applet can be presented here' +
'</p>' +
'</li>' +
'</ul>',
_render: function(applet) {
this._el.querySelector('#aid .value')
.textContent = applet.aid;
},
};
exports.AppletDetailsView = AppletDetailsView;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,136 @@
'use strict';
/* globals AppletListModel, AppletListView, Timer, SEUtils,
addMixin, DebugMixin */
/* exported AppletListController */
(function(exports) {
var AppletListController = function(simDataSource, id, view, filter, multipleAppletsActive){
this._simDataSource = simDataSource;
this._id = id;
this._filter = filter;
this._multipleAppletsActive = multipleAppletsActive;
this._view = view;
this._view.addListener(this);
addMixin(this, DebugMixin);
};
AppletListController.prototype = {
_id: null,
_viewId: null,
_multipleAppletsActive: false,
_simDataSource: null,
_model: null,
_view: null,
_filter: null,
init: function alc_init() {
return this.refreshModelView();
},
refreshModelView: function alc_refreshModelView() {
return this._refreshModel()
.then((simAccessTime) => {
var viewUpdateTime = this._refreshView();
this.debug('Applets read in ' + simAccessTime + ' ms. ' +
'View refreshed in ' + viewUpdateTime + ' ms.');
return simAccessTime;
});
},
// TODO unify AID arguments
handleHCIEvt: function alc_handleHCIEvt(hci, fastPayOn) {
var aid = SEUtils.byteToHexString(hci.aid);
var applet = this._model.getAppletByHexAID(aid);
if(!applet) {
this.debug('Got HCI from uknown aid '+ aid);
return;
}
this.debug('HCI EVT_TRANSACTION from ' + aid);
if(!fastPayOn) {
this._toggleAppletCLF(aid, false)
.catch((e) => console.error(e));
}
},
onEvent: function alc_onEvent(source, data) {
if(data.action === 'details-hide') {
this._view.hideDetails();
return;
}
if(data.action === 'details-show') {
this._view.showDetails(this._model.getAppletByID(data.id));
return;
}
// TODO add explicit event
var applet = this._model.getAppletByID(data.id);
var select = data.action === 'card-selection';
this._toggleAppletCLF(applet.aid, select);
},
showView: function alc_showView() {
this._view.visible = true;
},
hideView: function alc_hideView() {
this._view.visible = false;
},
_toggleAppletCLF(aid, toggle) {
this._view.disableCardSelection();
return this._simDataSource.toggleCLF(toggle, aid)
// TODO add comment about CREL and auto off
//.then(() => this._refreshModel())
.catch((e) => this.debug('Toggle fast pay failed', e))
.then((success) => {
if(success) {
this._model.applets.forEach((a) => {
if(a.aid === aid) {
a.contactlessState = toggle ? 1 : 0;
} else if (toggle && !this._multipleAppletsActive) {
a.contactlessState = 0;
}
});
}
this._view.updateSelection(this._model.applets);
this._view.enableCardSelection();
});
},
_refreshModel: function alc_refreshMode() {
var timer = new Timer();
return this._simDataSource.getAppletsData()
.then((applets) => {
var simAccTime = timer.getElapsedTime();
if(!this._model) {
this._model = new AppletListModel();
}
this._model.updateList(applets, this._filter);
return simAccTime;
});
},
_refreshView: function alc_refreshView() {
var timer = new Timer();
this._view.refreshView(this._model.applets);
return timer.getElapsedTime();
}
};
exports.AppletListController = AppletListController;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,60 @@
'use strict';
/* globals addMixin, ObserverSubjectMixin, CardSwipeMixin, getSamplePaymentImg,
ListViewMixin, AppletDetailsView, DebugMixin */
/* exported AppletListView */
(function(exports) {
var AppletListView = function(id, imgSrc, data) {
this._id = id;
addMixin(this, ObserverSubjectMixin);
addMixin(this, ListViewMixin);
addMixin(this, CardSwipeMixin);
addMixin(this, DebugMixin);
this.initListView(id, imgSrc, data);
this._detailsView = new AppletDetailsView(id + '-details');
};
AppletListView.prototype = {
_detailsVisible: false,
_detailsView: null,
refreshView: function(data) {
this.updateList(data);
if(!Array.isArray(data) || !data.length) {
return;
}
var tapCallback = (id) => {
var action = 'details-' + ((this._detailsVisible) ? 'hide' : 'show');
this._notify({ id: id, action: action});
};
var swipeCallback = (activeId) => {
if(this._detailsVisible) {
this.hideDetails();
}
};
this.initCardSwipe('li', tapCallback, swipeCallback);
this.updateSelection(data);
},
showDetails: function alv_showDetails(applet) {
this._el.querySelector('.slider').scrollTop = 0;
this._detailsView.refreshView(applet);
this._el.classList.add('details-visible');
this._detailsVisible = true;
},
hideDetails: function alv_hideDetails() {
this._el.classList.remove('details-visible');
this._detailsVisible = false;
},
};
exports.AppletListView = AppletListView;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,105 @@
'use strict';
/* globals SEUtils, TAG */
/* exported AppletModel, AppletListModel */
(function(exports) {
// TODO decide how to store data in the app
var AppletModel = function(id, aid, state){
this.id = id;
this.aid = SEUtils.byteToHexString(aid);
this.lifecycleState = state[0];
this.contactlessState = state[1];
};
AppletModel.prototype = {
// id in view
id: null,
aid: null,
lifecycleState: null,
contactlessState: null,
set state(value) {
this.lifecycleState = value[0];
this.contactlessState = value[1];
},
get selected() {
return this.contactlessState === 1;
},
get imgId() {
return this.aid;
}
};
var AppletListModel = function(appletsData, filter) {
if(appletsData) {
this.updateList(appletsData, filter);
}
};
AppletListModel.prototype = {
applets: [],
updateList: function alm_updateList(appletsData, filter) {
var parsedApplets = this._parseAppletList(appletsData);
parsedApplets = (!filter) ? parsedApplets : parsedApplets.filter(filter);
// now we create AppletModel objects and store them in the list
var refreshed = [];
parsedApplets.forEach((appl, idx) => {
var aidHex = SEUtils.byteToHexString(appl.aid);
var nAppl = this.getAppletByHexAID(aidHex);
if(nAppl) {
nAppl.state = appl.state;
} else {
nAppl = new AppletModel('appl'+idx, appl.aid, appl.state);
}
refreshed.push(nAppl);
});
this.applets = refreshed;
},
getAppletByID: function alm_getAppletByID(id) {
return this.applets.find((applet) => applet.id === id);
},
getAppletByHexAID: function alm_getAppletByHexAID(aid) {
return this.applets.find((applet) => applet.aid === aid);
},
_parseAppletList: function alm_parseAppletList(data) {
var list = [];
for(var i = 0, len = data.length; i < len;) {
if(data[i] === TAG.CRS.APP_TEMPLATE) {
var end = i+2 + data[i+1];
var appletData = data.subarray(i+2, end);
var applet = this._parseApplet(appletData);
if(applet) {
list.push(applet);
}
i = end;
}
}
return list;
},
_parseApplet: function alm_parseApplet(data) {
if (data[0] === TAG.CRS.AID) {
var applet = {
aid: data.subarray(2, 2 + data[1]),
state: data.subarray(data[1] + 5)
};
return applet;
}
},
};
exports.AppletModel = AppletModel;
exports.AppletListModel = AppletListModel;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,43 @@
'use strict';
/* exported HCIView */
(function(exports) {
const CLOSE_TIMEOUT = 1000;
var HCIView = function(id, imgSrc) {
this._el = document.querySelector('#'+id);
this._imgSrc = imgSrc;
};
HCIView.prototype = {
_el: null,
_imgSrc: null,
_clickHandler: null,
show: function show(aid, closeApp) {
window.navigator.vibrate([50, 50, 50]);
this._el.querySelector('#hci-image').src = this._imgSrc(aid);
this._el.querySelector('#hci-number').textContent = aid;
this._clickHandler = () => this._dismiss(closeApp);
this._el.addEventListener('click', this._clickHandler);
this._el.classList.add('show');
},
_dismiss: function _dismiss(closeApp) {
if(closeApp) {
setTimeout(() => window.close(), CLOSE_TIMEOUT);
return;
}
this._el.classList.remove('show');
this._el.removeEventListener('click', this._clickHandler);
},
};
exports.HCIView = HCIView;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,99 @@
'use strict';
/* exported CardSwipeMixin */
/* globals Swipeable */
(function(exports) {
/**
* Needs to be used with ObserverSubjectMixin
*/
var CardSwipeMixin = {
_swipeHandler: null,
initCardSwipe: function initCardSwipe(selector, tapCb, swipeCb, options) {
options = options || {
'element-width': 260,
'auto-scroll-trigger': 0.25,
'min-speed': 0.75
};
if(this._swipeHandler) {
this._swipeHandler.disable();
this._swipeHandler.onswipe = null;
this._swipeHandler.ontap = null;
this._swipeHandler.onvertswipe = null;
this._swipeHandler = null;
}
this._swipeHandler = new Swipeable(this._el, selector, options);
if(tapCb) {
this._swipeHandler.ontap = tapCb;
}
if(swipeCb) {
this._swipeHandler.onswipe = swipeCb;
}
this._swipeHandler.onvertswipe = (direction, id) => {
var itemEl = this._el.querySelector('#' + id);
var action = '';
if(direction === 'up' && !itemEl.classList.contains('selected')) {
action = 'card-selection';
this._changeItemSelection(itemEl, true);
}
if(direction === 'down' && itemEl.classList.contains('selected')) {
action = 'card-deselection';
this._changeItemSelection(itemEl, false);
}
if(action) {
this._notify({ id: id, action: action});
}
};
this._swipeHandler.enable();
},
updateSelection: function updateSelection(items) {
items.forEach((item, idx) => {
var itemEl = this._el.querySelector('#'+item.id);
if(item.selected && !itemEl.classList.contains('selected')) {
this._changeItemSelection(itemEl, true);
}
if(!item.selected && itemEl.classList.contains('selected')) {
this._changeItemSelection(itemEl, false);
}
});
},
enableCardSelection: function enablCardSelection() {
this._swipeHandler.unpauseSwipe();
},
disableCardSelection: function disableCardSelection() {
this._swipeHandler.pauseSwipe();
},
_changeItemSelection: function changeItemSelection(itemEl, select) {
var elIdx = this._swipeHandler._elts.indexOf(itemEl);
if(elIdx === -1) {
return;
}
if(select) {
itemEl.classList.remove('deselected');
itemEl.classList.add('selected');
this._swipeHandler.progressSelect(elIdx, true);
} else {
itemEl.classList.remove('selected');
itemEl.classList.add('deselected');
this._swipeHandler.progressSelect(elIdx, false);
}
},
};
exports.CardSwipeMixin = CardSwipeMixin;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,46 @@
'use strict';
/* exported DetailsViewMixin */
(function(exports) {
var DetailsViewMixin = {
_id: null,
_el: null,
_visible: false,
_template: '',
initDetailsView: function(id, data) {
this._id = id;
this._el = document.querySelector('#' + this._id);
this._el.innerHTML = this._template;
if(data) {
this._render(data);
}
},
isVisible: function() {
return this._visible;
},
show: function() {
this._visible = true;
this._el.classList.add(this._id+'-visible');
},
hide: function() {
this._visible = false;
this._el.classList.remove(this._id+'-visible');
},
_render: function(data) {},
refreshView: function(data) {
if(data) {
this._render(data);
}
}
};
exports.DetailsViewMixin = DetailsViewMixin;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,87 @@
'use strict';
/* globals CARD_UNKNOWN */
/* exported ListViewMixin */
(function(exports) {
var ListViewMixin = {
_id: null,
_el: null,
_visible: false,
_imgMap: null,
_itemTemplate:
'<div class="listview-element-pocket">' +
'<div class="listview-element-sew"></div>' +
'</div>' +
'<img class="listview-element-img" src="img/cards/unknown.png">',
initListView: function(id, imgMap, data) {
this._id = id;
this._el = document.querySelector('#' + this._id);
this._imgMap = imgMap;
if(data && data.length) {
this._render(data);
}
},
updateList(listItems) {
if(!Array.isArray(listItems) || !listItems.length) {
this.debug('list without elements', listItems);
this._renderEmpty();
return;
}
this._render(listItems);
},
isVisible: function() {
return this._visible;
},
show: function() {
this._visible = true;
this._el.classList.remove('listview-hide');
},
hide: function() {
this._visible = false;
this._el.classList.add('listview-hide');
},
_render: function(data){
var emptyLi = document.createElement('li');
emptyLi.innerHTML = this._itemTemplate;
var ulFragment = document.createDocumentFragment();
data.forEach((item) => {
var li = emptyLi.cloneNode(true);
li.id = item.id;
var src = this._imgMap(item.imgId);
// TODO remove CARD_UNKNOW dependency here
li.querySelector('img').src = src ? src : CARD_UNKNOWN;
ulFragment.appendChild(li);
});
var ul = this._el.querySelector('ul');
ul.innerHTML = '';
ul.appendChild(ulFragment);
},
_renderEmpty: function() {
var emptyLi = document.createElement('li');
emptyLi.innerHTML = this._itemTemplate;
var ul = this._el.querySelector('ul');
ul.innerHTML = '';
ul.appendChild(emptyLi);
},
};
exports.ListViewMixin = ListViewMixin;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,209 @@
'use strict';
/* globals GestureDetector */
/* exported Swipeable */
(function(exports) {
var Swipeable = function(container, eltSelector, options) {
this._container = container.querySelector('.swipeable-container');
this._eltSelector = eltSelector;
this.updateElements();
this._createProgress();
this._gestureDetector = new GestureDetector(this._container,
{ holdEvents: false });
this._panHandler = e => this._handlePan(e);
this._swipeHandler = e => this._handleSwipe(e);
this._container.addEventListener('tap', (e) => {
if(this._tapCallback) {
this._tapCallback(this._elts[this._activeEltIdx].id);
}
});
options = options || {};
this._elWidth = options['element-width'] || this._elts[0].offsetWidth;
this._triggerPoint = options['auto-scroll-trigger'] || this._triggerPoint;
this._minSpeed = options['min-speed'] || this._minSpeed;
};
Swipeable.prototype = {
_gestureDetector: null,
_container: null,
_eltSelector: null,
_elts: [],
_progressElts: null,
_elWidth: 0,
_triggerPoint: 0.25,
_minSpeed: 1.0,
_inTransition: false,
_offset: 0,
_activeEltIdx: 0,
_tapCallback: null,
_swipeCallback: null,
_vertSwipeCallback: null,
set ontap(callback) {
this._tapCallback = callback;
},
set onswipe(callback) {
this._swipeCallback = callback;
},
set onvertswipe(callback) {
this._vertSwipeCallback = callback;
},
enable: function() {
this._gestureDetector.startDetecting();
this.unpauseSwipe();
},
disable: function() {
this._gestureDetector.stopDetecting();
},
pauseSwipe: function() {
this._container.removeEventListener('pan', this._panHandler);
this._container.removeEventListener('swipe', this._swipeHandler);
},
unpauseSwipe: function() {
this._container.addEventListener('pan', this._panHandler);
this._container.addEventListener('swipe', this._swipeHandler);
},
updateElements: function() {
var nodeList = this._container.querySelectorAll('.swipeable > ' + this._eltSelector);
this._elts = Array.prototype.slice.call(nodeList);
},
_createProgress: function() {
if(!this._elts || !this._elts.length) {
return;
}
var progressDiv = this._container.querySelector('.swipeable-progress');
if(!progressDiv) {
return;
}
var progressFragment = document.createDocumentFragment();
this._progressElts = this._elts.map(() => {
var div = document.createElement('div');
progressFragment.appendChild(div);
return div;
});
this._progressElts[0].classList.add('swipeable-progress-active');
progressDiv.innerHTML = '';
progressDiv.appendChild(progressFragment);
},
_updateProgress: function() {
this._progressElts.forEach((elt, idx) => {
if(idx === this._activeEltIdx) {
elt.classList.add('swipeable-progress-active');
} else if(elt.classList.contains('swipeable-progress-active')) {
elt.classList.remove('swipeable-progress-active');
}
});
},
progressSelect: function progressSelect(idx, select) {
var pElt = this._progressElts[idx];
if(!pElt) {
return;
}
if(select) {
pElt.classList.add('swipeable-progress-selected');
} else if(pElt.classList.contains('swipeable-progress-selected')) {
pElt.classList.remove('swipeable-progress-selected');
}
},
_handlePan: function(event) {
if(this._inTransition) {
return;
}
if (Math.abs(event.detail.relative.dx) > Math.abs(event.detail.relative.dy)) {
this._offset += event.detail.relative.dx;
this._animate();
}
},
_handleSwipe: function(event) {
if(this._vertSwipeCallback &&
['up', 'down'].indexOf(event.detail.direction) !== -1) {
var id = this._elts[this._activeEltIdx].id;
this._vertSwipeCallback(event.detail.direction, id);
}
var elPos = Math.abs(this._offset)%this._elWidth;
var triggerPassed = elPos > this._elWidth * this._triggerPoint;
var velocity = event.detail.vx;
var fastEnough = Math.abs(velocity) > this._minSpeed;
var direction = (event.detail.direction === 'left') ? 1 : -1;
var newIdx = this._computeNewIndex(direction);
// Make sure that that the speed and pan amount are in the same direction
//var samedirection = velocity === 0 || this._offset / velocity >= 0;
if((triggerPassed || fastEnough) && newIdx !== this._activeEltIdx) {
var speed = Math.max(Math.abs(velocity), this._minSpeed);
var time = (this._elWidth - Math.abs(this._offset%this._elWidth)) / speed;
this._activeEltIdx = newIdx;
this._performScroll(time);
this._updateProgress();
if (this._swipeCallback) {
this._swipeCallback(this._activeEltIdx);
}
} else if(this._offset !== 0){
var time = Math.abs(this._offset%this._elWidth) / this._minSpeed;
this._performScroll(time);
if (this._swipeCallback) {
this._swipeCallback(this._activeEltIdx);
}
}
},
_animate: function(time) {
var handler = (e) => {
e.target.style.transition = null;
e.target.removeEventListener('transitionend', handler);
};
this._elts.forEach((el) => {
if(time) {
el.style.transition = 'transform ' + time + 'ms ease';
el.addEventListener('transitionend', handler);
}
el.style.transform = 'translate3d('+this._offset+'px,0,0)';
});
},
_performScroll: function(time) {
this._inTransition = true;
this._offset = -1 * this._activeEltIdx * this._elWidth;
this._animate(time);
setTimeout(() => { this._inTransition = false; }, time);
},
_computeNewIndex: function(direction) {
var index = this._activeEltIdx + direction;
var offBounds = index < 0 || index >= this._elts.length;
return offBounds ? this._activeEltIdx : index;
}
};
exports.Swipeable = Swipeable;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,82 @@
'use strict';
/* exported Timer, addMixin, ObserverSubjectMixin, DebugMixin */
(function(exports) {
function addMixin(destination, source) {
for (var k in source) {
if (source.hasOwnProperty(k) && !(k in destination)) {
destination[k] = source[k];
}
}
return destination;
}
var DebugMixin = {
DEBUG: true,
debug: function(msg, obj) {
if(!this.DEBUG) {
return;
}
var str = (this._id) ? ('[' + this._id + '] ' + msg) : msg;
if(obj) {
str += '|' + JSON.stringify(obj);
}
console.log(str);
}
};
var ObserverSubjectMixin = {
_notify: function mlv_notify(data) {
if(this._observers && Array.isArray(this._observers)) {
this._observers.forEach((obs) => obs.onEvent(this._id, data));
}
},
addListener: function mlv_addListener(observer) {
if('onEvent' in observer) {
if(!this._observers) {
this._observers = [];
}
this._observers.push(observer);
}
},
removeListener: function mlv_removeListener(observer) {
if(!this._observers || !Array.isArray(this._observers)) {
return;
}
var idx = this._observers.indexOf(observer);
if(idx !== -1) {
this._observers.splice(idx, 1);
}
},
};
var Timer = function() {
this._startTime = new Date();
};
Timer.prototype = {
_startTime: null,
getElapsedTime: function getElapsedTime() {
var endTime = new Date();
return endTime.getTime() - this._startTime.getTime();
},
reset: function reset() {
this._startTime = new Date();
}
};
exports.addMixin = addMixin;
exports.DebugMixin = DebugMixin;
exports.ObserverSubjectMixin = ObserverSubjectMixin;
exports.Timer = Timer;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,24 @@
'use strict';
/* exported ERRORS */
(function(exports) {
const ERRORS = {
SIM: {
NOT_IDLE: 'Wrong sim access state.',
ACCESS_FAILED: 'Failed to enable SIM access.',
NO_READER: 'No reader with SE present.',
APPLETS_FAILURE: 'Failed to get applets data.',
PIN_FAILURE: 'Failed to issue PIN command.',
},
GLOBAL: {
NO_API: 'No Secure Element API present.',
NO_SIM: 'No SIM present.',
SIM_FAILURE: 'SIM failure.',
UNKNOWN: 'Unknown error.'
}
};
exports.ERRORS = ERRORS;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,73 @@
'use strict';
/* globals ERRORS, addMixin, ObserverSubjectMixin */
(function(exports) {
const ERROR_VIEW_ID = {};
ERROR_VIEW_ID[ERRORS.GLOBAL.NO_API] = 'no-api';
ERROR_VIEW_ID[ERRORS.GLOBAL.NO_SIM] = 'no-sim';
ERROR_VIEW_ID[ERRORS.GLOBAL.SIM_FAILURE] = 'sim-error';
ERROR_VIEW_ID[ERRORS.GLOBAL.Unknown] = 'unknown-error';
var ErrorView = function(id) {
this._el = document.querySelector('#' + id);
addMixin(this, ObserverSubjectMixin);
var settingsBtn = this._el.querySelector('.error-settings-btn');
settingsBtn.addEventListener('click',
() => this._notify({ action: 'open-settings'}));
};
ErrorView.prototype = {
_el: null,
show: function() {
this._el.classList.remove('hide');
},
hide: function() {
this._el.classList.add('hide');
},
handleError: function(error) {
console.error(error);
var globalError = this._mapErrorToGlobal(error);
this._showGlobalError(globalError);
this.show();
},
_mapErrorToGlobal: function(error) {
if(Object.keys(ERRORS.GLOBAL).find(k => ERRORS.GLOBAL[k] === error)) {
return error;
}
// mapping no reader to no SIM
if(ERRORS.SIM.NO_READER === error) {
return ERRORS.GLOBAL.NO_SIM;
}
if(Object.keys(ERRORS.SIM).find(k => ERRORS.SIM[k] === error)) {
return ERRORS.GLOBAL.SIM_FAILURE;
}
return ERRORS.GLOBAL.UNKNOWN;
},
_showGlobalError: function(globalError) {
var errorViewId = ERROR_VIEW_ID[globalError];
var nodesList = this._el.querySelectorAll('article');
[].slice.call(nodesList).forEach(e => {
if(e.id === errorViewId) {
e.classList.remove('hide');
} else {
e.classList.add('hide');
}
});
},
};
exports.ErrorView = ErrorView;
}(window));

View file

@ -0,0 +1,66 @@
'use strict';
/* globals SEUtils */
/* exported PAYMENT_IMG_SRC, NONPAYMENT_IMG_SRC, PAYMENT_APPLET_FILTER,
NONPAYMENT_APPLET_FILTER, CARD_UNKNOWN */
(function(exports) {
const CARD_UNKNOWN = 'img/cards/unknown.png';
const SAMPLE_NONPAYMENT_IMG = [
'img/cards/other-1.png',
'img/cards/other-2.png',
'img/cards/other-3.png'
];
const SAMPLE_PAYMENT_IMG = [
'img/cards/payment-1.png',
'img/cards/payment-2.png',
'img/cards/payment-3.png'
];
var _getSampleImg = function(imageArray) {
var counter = 0;
var imgMap = {};
return function(id) {
var imgSrc = imgMap[id];
if(imgSrc) {
return imgSrc;
}
counter = (imageArray.length - 1) === counter ? 0 : ++counter;
if(id) {
imgMap[id] = imageArray[counter];
}
return imageArray[counter];
};
};
var PAYMENT_IMG_SRC = _getSampleImg(SAMPLE_PAYMENT_IMG);
var NONPAYMENT_IMG_SRC = _getSampleImg(SAMPLE_NONPAYMENT_IMG);
var PAYMENT_APPLET_FILTER = function(appletData) {
var hexAid = SEUtils.byteToHexString(appletData.aid);
var hexState = SEUtils.byteToHexString(appletData.state);
// hex states for payment applets
// 1F00 - personalised, not active on Contactless interface (CLF)
// 1F01 - personalised, active on CLF
// 0708 - not personalised, not possible to use in payment
return hexAid.startsWith('A000000004') &&
(hexState === '1F00' || hexState === '1F01');
};
var NONPAYMENT_APPLET_FILTER = function(appletData) {
var hexAid = SEUtils.byteToHexString(appletData.aid);
return hexAid.startsWith('D');
};
exports.CARD_UNKNOWN = CARD_UNKNOWN;
exports.PAYMENT_IMG_SRC = PAYMENT_IMG_SRC;
exports.NONPAYMENT_IMG_SRC = NONPAYMENT_IMG_SRC;
exports.PAYMENT_APPLET_FILTER = PAYMENT_APPLET_FILTER;
exports.NONPAYMENT_APPLET_FILTER = NONPAYMENT_APPLET_FILTER;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,316 @@
'use strict';
/* globals addMixin, DebugMixin, PinView, DEFAULTS, PIN_DEFAULTS */
/* exported PinController */
(function(exports) {
const PIN_STATE = {
UNKNOWN: 'unknown',
ENABLED: 'enabled',
DISABLED: 'disabled',
BLOCKED: 'blocked',
};
var _defaultPin = DEFAULTS.PIN_VALUE;
var PinController = function(simDataSource, id, viewId, templateId) {
this._simDataSource = simDataSource;
this._id = id;
this._view = new PinView(viewId, PIN_DEFAULTS.PIN_LEN, templateId);
addMixin(this, DebugMixin);
};
PinController.prototype = {
_id: null,
_simDataSource: null,
_view: null,
// TODO consider removing
_simPinState: PIN_STATE.UNKNOWN,
_pinCheckInProgress: false,
_lastInputedPin: null,
get pinCheckInProgress() {
return this._pinCheckInProgress;
},
set defaultPin(value) {
_defaultPin = value;
},
get view() {
return this._view._el;
},
hideView: function() {
this._view.hide();
},
verifyPin: function() {
return this._requestPinVerification().then((result) => {
if(result) {
this.hideView();
}
return result;
});
},
enablePin: function() {
return this._requestPinVerification()
.then((verified) => {
if(verified) {
this.debug('Pin verified');
this._simPinState = PIN_STATE.ENABLED;
this._lastInputedPin = null;
return true;
} else {
this.debug('Pin verification failed, pin probably disabled');
var pin = this._lastInputedPin;
this._lastInputedPin = null;
return this._enableSimPin(pin);
}
});
},
disablePin: function() {
return this._requestPinVerification()
.then((verified) => {
if(verified) {
this.debug('Pin verified');
var pin = this._lastInputedPin;
this._lastInputedPin = null;
return this._disableSimPin(pin);
} else {
this.debug('Pin verification failed, pin probably disabled');
this._lastInputedPin = null;
return true;
}
});
},
changePin: function() {
var pin;
return this._requestPinVerification(this._view.REQUEST_BEFORE_CHANGE)
.then((verified) => {
if(!verified) {
this.debug('Pin not verfied, not changing');
this._lastInputedPin = null;
return false;
}
pin = this._lastInputedPin;
this._lastInputedPin = null;
return this._getNewPin();
})
.then((newPin) => {
this.debug('New pin value ' + newPin);
var pinBytes = this._formatPin(pin);
var newPinBytes = this._formatPin(newPin);
return this._simDataSource.changePIN(pinBytes, newPinBytes);
})
.then((result) => this._handlePinChangeResult(result));
},
cancelPinAction: function() {
this._view.cancelPinRequest();
},
_handlePinChangeResult: function(result) {
this.debug('Pin change result ' + result);
if(result === '9000') {
return true;
} else {
this.debug('Unexpected error');
return false;
}
},
_getNewPin: function(prevAttempFailed) {
var newPin;
return this._getUserPin(PIN_DEFAULTS.MAX_PUK_ATTEMPTS, prevAttempFailed,
this._view.REQUEST_NEW_PIN)
.then((pin1) => {
newPin = pin1;
return this._getUserPin(PIN_DEFAULTS.MAX_PUK_ATTEMPTS, false,
this._view.REQUEST_CONFIRM);
})
.then((pin2) => {
if(newPin === pin2) {
return newPin;
}
this.debug('Pins do not match');
return this._getNewPin(true);
});
},
_requestPinVerification: function(reqType) {
this.debug('_requestPinVerification, SIM PIN : ' + this._simPinState);
// TODO verify if we need this
//if(this._pinCheckInProgress) {
// return Promise.resolve(false);
//}
this._pinCheckInProgress = true;
this._view.show();
return this._performVerification(PIN_DEFAULTS.MAX_PIN_ATTEMPTS, false, reqType)
.then((verified) => {
if(verified) {
//this._view.hide();
// TODO think this through
this._pinCheckInProgress = false;
}
return verified;
});
},
_getUserPin: function(attemptsLeft, lastVerFailed, reqType, pinLen) {
return new Promise((resolve, reject) => {
var listener = {
onEvent: (id, data) => {
this._view.removeListener(listener);
if (data.pin) {
this.debug('got PIN: ' + data.pin);
resolve(data.pin);
} else {
reject('pin-cancelled');
}
}
};
this._view.addListener(listener);
if(!this._view.isVisible) {
this._view.show();
}
this.debug('waiting for PIN input from user');
this._view.requestPin(attemptsLeft, lastVerFailed, reqType, pinLen);
});
},
_performVerification: function(attemptsLeft, lastVerFailed, reqType) {
return this._getUserPin(attemptsLeft, lastVerFailed, reqType)
.then((pin) => {
this._lastInputedPin = pin;
var pinBytes = this._formatPin(pin);
this.debug('verifying PIN ' + pinBytes);
return this._simDataSource.verifyPIN(pinBytes);
})
.then((result) => this._handleVerficationResult(result));
//TODO should we catch here?
},
_handleVerficationResult: function(result) {
this.debug('Verification finished, status word:' + result);
if(result === '9000') {
return true;
} else if(result === '63C0' || result === '6983') {
this.debug('Pin blocked');
return this._handlePinBlocked();
} else if(result.startsWith('63C')) {
var attemptsLeft = Number.parseInt(result[3]);
this.debug('Verification failed, tries left: ' + attemptsLeft);
return this._performVerification(attemptsLeft, true);
} else {
this.debug('Unknown status word, PIN probably not enabled');
return false;
}
},
_handlePinBlocked: function() {
this._simPinState = PIN_STATE.BLOCKED;
this.debug('PUK unblock flow start.');
var unblockRequested = () => this._performPinUnblock(PIN_DEFAULTS.MAX_PUK_ATTEMPTS);
return this._view.blockPin().then(unblockRequested);
},
_performPinUnblock: function(attemptsLeft, lastUnblockFailed) {
var puk;
return this._getUserPin(attemptsLeft, lastUnblockFailed,
this._view.REQUEST_PUK, PIN_DEFAULTS.MAX_SIM_PIN_LEN)
.then((inputedPuk) => {
this.debug('Inputed PUK ' + inputedPuk);
puk = inputedPuk;
return this._getNewPin(false);
})
.then((inputedPin) => {
this.debug('Inputed PIN ' + inputedPin);
var bytePuk = this._formatPin(puk);
var bytePin = this._formatPin(inputedPin);
// if we're part of disable/enable flow we need to save pin here
this._lastInputedPin = inputedPin;
return this._simDataSource.unblockPIN(bytePuk, bytePin);
})
.then((result) => this._handlePinUnblockResult(result));
},
_handlePinUnblockResult: function(result) {
this.debug('PIN unblock result ' + result);
if(result === '9000') {
return true;
} else if(result === '63C0' || !result.startsWith('63C')) {
this.debug('Complate block');
return this._view.blockPuk();
} else {
var attemptsLeft = Number.parseInt(result[3]);
this.debug('PIN unblock failed, tries left: ' + attemptsLeft);
return this._performPinUnblock(attemptsLeft, true);
}
},
_enableSimPin: function(pin) {
var pinBytes = this._formatPin(pin);
return this._simDataSource.enablePIN(pinBytes)
.then((result) => this._handlePinEnabledResult(result));
},
_handlePinEnabledResult: function(result) {
this.debug('PIN enable result ' + result);
if(result === '9000') {
this._simPinState = PIN_STATE.ENABLED;
//this._view.hide();
return true;
} else if(result === '63C0' || result === '6983') {
this.debug('Pin blocked');
return this._handlePinBlocked();
} else {
var tries = result.startsWith('63C') ? Number.parseInt(result[3]) : 0;
this._view.showEnableFailed(tries, _defaultPin);
return false;
}
},
_disableSimPin: function(pin) {
var pinBytes = this._formatPin(pin);
return this._simDataSource.disablePIN(pinBytes)
.then((result) => this._handlePinDisableResult(result));
},
_handlePinDisableResult: function(result) {
this.debug('PIN disable result ' + result);
if(result === '9000') {
this._simPinState = PIN_STATE.DISABLED;
//this._view.hide();
return true;
}
return false;
},
_formatPin: function(pin) {
var pinBytes = pin.split('').map((d) => d.charCodeAt());
for(var i = pinBytes.length; i < PIN_DEFAULTS.MAX_SIM_PIN_LEN; i++) {
pinBytes.push(PIN_DEFAULTS.PIN_PADDING);
}
return pinBytes;
}
};
exports.PinController = PinController;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,274 @@
'use strict';
/* globals addMixin, ObserverSubjectMixin, DebugMixin */
/* exported PinView */
(function(exports) {
// number of pin input attempts which should
// trigger the pin warning display
const PIN_ATTEMPTS_WARNING = 2;
const PIN_INCORRECT_TIMEOUT = 1000; //ms
const PIN_DEFAULT_LEN = 4;
const PIN_REQ_DEF = 'request-default-pin';
const PIN_REQ_BEFORE_CHANGE = 'request-pin-before-change';
const PIN_REQ_PUK = 'request-puk';
const PIN_REQ_NEW_PIN = 'request-new-pin';
const PIN_REQ_REPEAT_NEW_PIN = 'request-repeat-new-pin';
const PIN_ENTRY_TEXT_MAP = {};
PIN_ENTRY_TEXT_MAP[PIN_REQ_DEF] = {
entry: 'Please enter your PIN',
error: 'Incorrect PIN'
};
PIN_ENTRY_TEXT_MAP[PIN_REQ_BEFORE_CHANGE] = {
entry: 'Please enter current PIN',
error: 'Incorrect PIN'
},
PIN_ENTRY_TEXT_MAP[PIN_REQ_PUK] = {
entry: 'Please enter your PUK',
error: 'Incorrect PUK'
};
PIN_ENTRY_TEXT_MAP[PIN_REQ_NEW_PIN] = {
entry: 'Please enter new PIN',
error: 'New PIN mismatch',
};
PIN_ENTRY_TEXT_MAP[PIN_REQ_REPEAT_NEW_PIN] = {
entry: 'Please confirm new PIN',
error: ''
};
var PinView = function(id, defPinLen, templateId) {
this._id = id;
this._defaultPinLen = defPinLen || PIN_DEFAULT_LEN;
this._maxPinLen = this._defaultPinLen;
addMixin(this, ObserverSubjectMixin);
addMixin(this, DebugMixin);
if(templateId) {
this._el = document.querySelector('#' + templateId).cloneNode(true);
this._el.id = this._id;
} else {
this._el = document.querySelector('#' + this._id);
}
this._init();
};
PinView.prototype = {
REQUEST_PIN: PIN_REQ_DEF,
REQUEST_BEFORE_CHANGE: PIN_REQ_BEFORE_CHANGE,
REQUEST_PUK: PIN_REQ_PUK,
REQUEST_NEW_PIN: PIN_REQ_NEW_PIN,
REQUEST_CONFIRM: PIN_REQ_REPEAT_NEW_PIN,
_el: null,
_id: null,
_visible: false,
_pinInputEl: null,
_inputedPin: '',
// we have a default PIN length but we can do one time request for PINs
// of different length (i.e. PUK)
_defaultPinLen: 0,
_maxPinLen: 0,
_inputBlocked: false,
isVisible() {
return this._visible;
},
show: function() {
this._visible = true;
this._el.classList.remove('hide');
this._clearPin();
},
hide: function() {
this._visible = false;
this._el.classList.add('hide');
},
_init: function() {
this._pinInputEl = this._el.querySelector('.pin-input');
var keyHandler = this._keyHandler.bind(this);
var keypad = this._el.querySelector('.pin-keypad');
keypad.addEventListener('touchstart', keyHandler, true);
this._updatePinInput();
},
_keyHandler: function(event) {
event.preventDefault();
event.stopPropagation();
var key = event.target.dataset.value;
if(!key || this._inputBlocked) {
return;
}
switch(key) {
case 'clear': this._clearPin(); break;
case 'backspace': this._backspacePin(); break;
default: this._appendPin(key);
}
},
_clearPin: function() {
this._inputedPin = '';
this._updatePinInput();
},
_backspacePin: function() {
if(!this._inputedPin.length) {
return;
}
this._inputedPin = this._inputedPin.slice(0, -1);
this._updatePinInput();
},
_appendPin: function(digit) {
if(this._inputedPin.length >= this._maxPinLen) {
return;
}
this._inputedPin += digit;
this._updatePinInput();
if(this._inputedPin.length === this._maxPinLen) {
if(this._maxPinLen !== this._defaultPinLen) {
this._maxPinLen = this._defaultPinLen;
}
this._notify({ action: 'pin-inputed', pin: this._inputedPin });
}
},
_updatePinInput: function() {
var inputedDigits = this._inputedPin.length;
var fragment = document.createDocumentFragment();
for(var i = 0; i < this._maxPinLen; i++) {
var pinDigit = document.createElement('div');
if(i < inputedDigits) {
pinDigit.classList.add('pin-digit-entered');
}
fragment.appendChild(pinDigit);
}
this._pinInputEl.innerHTML = '';
this._pinInputEl.appendChild(fragment);
},
_updatePinInputTexts: function _updatePinInputTexts(reqType) {
reqType = (reqType in PIN_ENTRY_TEXT_MAP) ? reqType : PIN_REQ_DEF;
this._el.querySelector('.pin-entry-text')
.textContent = PIN_ENTRY_TEXT_MAP[reqType].entry;
this._el.querySelector('.pin-entry-error-text')
.textContent = PIN_ENTRY_TEXT_MAP[reqType].error;
},
requestPin: function(attemptsLeft, showPinIncorrect, reqType, pinLen) {
this.debug('requestPIN, tries: ' + attemptsLeft + ', type: ' + reqType);
this._maxPinLen = pinLen || this._maxPinLen;
this._clearPin();
this._updatePinInputTexts(reqType);
this._showByClass('pin-unblocked');
if(showPinIncorrect) {
this._showPinIncorrect();
if(reqType !== PIN_REQ_NEW_PIN) {
this._showWarning(attemptsLeft);
}
} else if(attemptsLeft <= PIN_ATTEMPTS_WARNING) {
this._showWarning(attemptsLeft);
} else {
this._clearWarning();
}
},
cancelPinRequest: function() {
this._maxPinLen = this._defaultPinLen;
this._notify({});
},
blockPin: function() {
window.navigator.vibrate([200, 100, 200, 100, 200]);
this._showByClass('pin-blocked');
return new Promise((resolve, reject) => {
var unblockBtn = this._el.querySelector('.pin-unblock-btn');
var onclick = () => {
unblockBtn.removeEventListener('click', onclick);
resolve();
};
unblockBtn.addEventListener('click', onclick);
});
},
blockPuk: function() {
window.navigator.vibrate([200, 100, 200, 100, 200]);
this._showByClass('puk-blocked');
return new Promise(()=>{});
},
showEnableFailed: function(tries, defaultPin) {
window.navigator.vibrate([200, 100, 200, 100, 200]);
this._showByClass('pin-enable-error');
this._el.querySelector('.pin-default-val').textContent = defaultPin;
if(tries || tries === 0) {
this._el.querySelector('.pin-enable-warning').classList.remove('hide');
var att = (tries === 1) ? ' entry attempt' : ' entry attempts';
this._el.querySelector('.pin-enable-attempt').textContent = tries + att;
} else {
this._el.querySelector('.pin-enable-warning').classList.add('hide');
}
},
_showByClass: function(clss) {
var nodesList = this._el.querySelectorAll('article');
Array.prototype.slice.call(nodesList).forEach(el => {
if(el.classList.contains(clss)) {
el.classList.remove('hide');
} else {
el.classList.add('hide');
}
});
},
_showWarning: function(attemptsLeft) {
this._el.querySelector('.pin-warning').classList.remove('hide');
var warning = attemptsLeft + ' attempt';
warning = (attemptsLeft !== 1) ? (warning + 's') : warning;
warning += ' remaining';
this._el.querySelector('.pin-attempts').textContent = warning;
},
_togglePinIncorrect: function(show) {
this._inputBlocked = show;
this._el.querySelector('.pin-entry').classList.toggle('hide');
this._el.querySelector('.pin-incorrect').classList.toggle('hide');
},
_showPinIncorrect: function() {
window.navigator.vibrate([200, 100, 200]);
this._togglePinIncorrect(true);
setTimeout(() => this._togglePinIncorrect(false), PIN_INCORRECT_TIMEOUT);
},
_clearWarning: function() {
this._el.querySelector('.pin-warning').classList.add('hide');
}
};
exports.PinView = PinView;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,116 @@
/*
CRS AID, TAGS and APDUs based on:
GlobalPlatform Card Technology Contactless Services Card Specification v2.2
– Amendment C Version 1.1.1
UICC APDUs based on:
ETSI TS 102 221 V8.2.0 (2009-06)
Smart Cards; UICC-Terminal interface; Physical and logical characteristics
(Release 8)
*/
/* globals SEUtils */
/* exported APDU, AID, TAG */
'use strict';
(function(exports) {
const AID = {
CRS: 'A00000015143525300',
UICC: 'A0000000871001FF33FFFF8901010100'
};
const APDU = {
// gets the response from SIM, can be used to retireve channel select
getResponse: { cla: 0x00, ins: 0xC0, p1: 0x00, p2: 0x00 },
readBinary: { cla: 0x00, ins: 0xB0, p1: 0x00, p2: 0x00 },
CRS: {
// Get applet version number and global update counter
getData: { cla: 0x80, ins: 0xCA, p1: 0x00, p2: 0xA5 },
// Get status of applets and Security Domains only.
// The ISD is ignored. Get first or only occurrence
getStatusAll1st: {
cla: 0x80, ins: 0xF2, p1: 0x40, p2: 0x00,
data: new Uint8Array([
0x4F, 0x00, // applet AID, empty here
0x5C, 0x03, // tag list to be returned
0x4F, // AID
0x9F, 0x70 // applet life cycle state - first byte;
// contactless activation state - second byte
])
},
// Get status of applets and Security Domains only.
// The ISD is ignored. Get next occurrence(s), if SW is 63h10h
getStatusAllNext: { cla: 0x80, ins: 0xF2, p1: 0x40, p2: 0x01,
data: new Uint8Array([0x4F, 0x00, 0x5C, 0x03, 0x4F, 0x9F, 0x70]) },
// Get status of applet identified with AID
getStatusAID: (aid) => {
return { cla: 0x80, ins: 0xF2, p1: 0x40, p2: 0x00,
data: SEUtils.joinUint8Arrays(
[0x4F, 0x0F], // applet AID
SEUtils.hexStringToByte(aid),
[0x5C, 0x04, // tag list
0x4F, // AID
0x9F, // applet lifecycle state
0x70, // contactless activation state
0x81 // selection priority
]
)
};
},
activateCLF: (aid) => {
return { cla: 0x80, ins: 0xF0, p1: 0x01, p2: 0x01,
data: SEUtils.joinUint8Arrays([0x4F, 0x0F],
SEUtils.hexStringToByte(aid))
};
},
deactivateCLF: (aid) => {
return { cla: 0x80, ins: 0xF0, p1: 0x01, p2: 0x00,
data: SEUtils.joinUint8Arrays([0x4F, 0x0F],
SEUtils.hexStringToByte(aid))
};
},
nfcActivate: { cla: 0x80, ins: 0xF0, p1: 0x04, p2: 0x80,
data: new Uint8Array([0x80, 0x01, 0x40]) },
nfcDeactivate: { cla: 0x80, ins: 0xF0, p1: 0x04, p2: 0x00,
data: new Uint8Array([0x80, 0x01, 0x40]) }
},
// PIN commands
UICC: {
getAttempts: (p2) => {
return { cla: 0x00, ins: 0x20, p1: 0x00, p2: p2 };
},
verify: (p2, pinBytes) => {
return { cla: 0x00, ins: 0x20, p1: 0x00, p2: p2, data: pinBytes };
},
enable: (p2, defPinBytes) => {
return { cla: 0x00, ins: 0x28, p1: 0x00, p2: p2, data: defPinBytes };
},
disable: (p2, defPinBytes) => {
return { cla: 0x00, ins: 0x26, p1: 0x00, p2: p2, data: defPinBytes };
},
change: (p2, curPinBytes, newPinBytes) => {
var data = SEUtils.joinUint8Arrays(curPinBytes, newPinBytes);
return { cla: 0x00, ins: 0x24, p1: 0x00, p2: p2, data: data };
},
unblock: (p2, pukBytes, defPinBytes) => {
var data = SEUtils.joinUint8Arrays(pukBytes, defPinBytes);
return { cla: 0x00, ins: 0x2C, p1: 0x00, p2: p2, data: data };
}
}
};
const TAG = {
CRS: {
APP_TEMPLATE: 0x61,
AID: 0x4F,
STATE: 0x9F70
},
};
exports.AID = AID;
exports.APDU = APDU;
exports.TAG = TAG;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,90 @@
'use strict';
/* exported SEUtils */
(function(exports) {
var SEUtils = {
byteToHexString: function byteToHexString(uint8arr) {
if (!uint8arr) {
return '';
}
var hexStr = '';
for (var i = 0; i < uint8arr.length; i++) {
var hex = (uint8arr[i] & 0xff).toString(16);
hex = (hex.length === 1) ? '0' + hex : hex;
hexStr += hex;
}
return hexStr.toUpperCase();
},
hexStringToByte: function hexStringToByte(str) {
var a = [];
for(var i = 0, len = str.length; i < len; i+=2) {
a.push(parseInt(str.substr(i,2),16));
}
return new Uint8Array(a);
},
byteToASCIIString: function byteToASCIIString(arr) {
var str = '';
var len = arr && arr.length ? arr.length : 0;
for(var i = 0; i < len; i++) {
str += arr[i] < 0x20 || arr > 0x7E ? '' : String.fromCharCode(arr[i]);
}
return str;
},
joinUint8Arrays: function joinUint8Arrays() {
var args = Array.prototype.slice.call(arguments);
var length = args.reduce(function(a, b) { return a + b.length; }, 0);
var out = new Uint8Array(length);
args.reduce(function(previousLen, buffer) {
out.set(buffer, previousLen);
return previousLen + buffer.length;
}, 0);
return out;
},
// no support for multibyte tag and constructed data object
parseSimpleTLV: function parseSimpleTLV(data) {
if(!data) {
return null;
}
var result = {};
for(var i = 0, len = data.length; i < len;) {
var tag = data[i];
var length = data[i+1];
var value = data.slice(i+2, i+2 + length);
result[tag] = value;
i += 2 + length;
}
return result;
},
promises: {
whilePromise: function(condition, action, init) {
return new Promise((resolve, reject) => {
var loop = (value) => {
if (condition(value)) {
return resolve(value);
}
return action(value).then(loop).catch(reject);
};
loop(init);
});
},
},
};
exports.SEUtils = SEUtils;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,187 @@
'use strict';
/* globals SEUtils, APDU, ERRORS, DEFAULTS */
/* exported simAccessManager */
(function(exports) {
const SIM_ACCESS_DISABLED = 'disabled';
const SIM_ACCESS_IDLE = 'idle';
const SIM_ACCESS_BUSY = 'busy';
var _simState = SIM_ACCESS_DISABLED;
var _seSession = null;
var _crsAid = DEFAULTS.CRS_AID;
var _uiccAid = DEFAULTS.UICC_AID;
var _pinP2 = DEFAULTS.PIN_P2;
var SIMAccessManager = {
set crsAid(value) {
_crsAid = value;
},
get crsAidBytes() {
return SEUtils.hexStringToByte(_crsAid);
},
set uiccAid(value) {
_uiccAid = value;
},
get uiccAidBytes() {
return SEUtils.hexStringToByte(_uiccAid);
},
set pinP2(value) {
_pinP2 = value;
},
get pinP2Bytes() {
return SEUtils.hexStringToByte(_pinP2)[0];
},
start: function start() {
if(!navigator.seManager) {
return Promise.reject(ERRORS.GLOBAL.NO_API);
}
return navigator.seManager.getSEReaders()
.then((readers) => {
var reader = readers.find(r => r.type.toLowerCase() === 'uicc' && r.isSEPresent);
if(!reader) {
return Promise.reject(ERRORS.SIM.NO_READER);
}
return reader.openSession();
})
.then((session) => {
_seSession = session;
_simState = SIM_ACCESS_IDLE;
})
.catch((e) => {
console.error(e);
_simState = SIM_ACCESS_DISABLED;
if(e !== ERRORS.SIM.NO_READER) {
e = ERRORS.SIM.ACCESS_FAILED;
}
return Promise.reject(e);
});
},
stop: function stop() {
return _seSession.closeAll()
.then(() => {
_seSession = null;
_simState = SIM_ACCESS_DISABLED;
})
.catch(e => this._cleanup(e, ERRORS.SIM.SIM_FAILURE));
},
_initSIMAccess: function _initSIMAccess() {
if(_simState !== SIM_ACCESS_IDLE) {
return Promise.reject(ERRORS.SIM.NOT_IDLE);
}
if(!_seSession.reader.isSEPresent) {
return Promise.reject(ERRORS.SIM.NO_READER);
}
_simState = SIM_ACCESS_BUSY;
return Promise.resolve();
},
_cleanup: function _cleanup(error, message) {
console.error(error);
_simState = SIM_ACCESS_IDLE;
if(error === ERRORS.SIM.NOT_IDLE || error === ERRORS.SIM.NO_READER) {
message = error;
}
return Promise.reject(message);
},
getAppletsData: function getAppletsData() {
return this._initSIMAccess()
.then(() => _seSession.openLogicalChannel(this.crsAidBytes))
.then((channel) => {
return channel.transmit(APDU.CRS.getStatusAll1st);
})
.then((resp) => {
var receivedData = new Uint8Array();
// we can get different responses, also propriatary
// using counter to not allow to try to query the SIM indifinately
var count = 0;
var condition = (resp) => (count++ > 10 || resp.sw1 === 0x90 && resp.sw2 === 0x00);
var action = (resp) => {
receivedData = SEUtils.joinUint8Arrays(receivedData, resp.data);
return resp.channel.transmit(APDU.CRS.getStatusAllNext);
};
return SEUtils.promises.whilePromise(condition, action, resp)
.then((lastResp) => {
lastResp.channel.close();
_simState = SIM_ACCESS_IDLE;
return SEUtils.joinUint8Arrays(receivedData, lastResp.data);
});
})
.catch(e => this._cleanup(e, ERRORS.SIM.APPLETS_FAILURE));
},
toggleCLF: function toggleCLF(on, aid) {
var apdu = on ? APDU.CRS.activateCLF(aid) : APDU.CRS.deactivateCLF(aid);
return this._initSIMAccess()
.then(() => _seSession.openLogicalChannel(this.crsAidBytes))
.then((channel) => channel.transmit(apdu))
.then((resp) => {
return resp.channel.close()
.then(() => {
_simState = SIM_ACCESS_IDLE;
return resp.sw1 === 0x90 && resp.sw2 === 0x00;
});
})
.catch(e => this._cleanup(e, ERRORS.SIM.SIM_FAILURE));
},
_execPinCommand: function _execPinCommand(type, pin1, pin2) {
return this._initSIMAccess()
.then(() => _seSession.openLogicalChannel(this.uiccAidBytes))
.then((channel) => {
var apdu = APDU.UICC[type](this.pinP2Bytes, pin1, pin2);
if(!apdu) {
return Promise.reject('PIN APDU type ' + type + ' not found');
}
return channel.transmit(apdu);
})
.then((resp) => {
return resp.channel.close().then(() => {
_simState = SIM_ACCESS_IDLE;
return SEUtils.byteToHexString([resp.sw1, resp.sw2]);
});
})
.catch(e => this._cleanup(e, ERRORS.SIM.PIN_FAILURE));
},
verifyPIN: function verifyPIN(pin) {
return this._execPinCommand('verify', pin);
},
enablePIN: function enablePIN(pin) {
return this._execPinCommand('enable', pin);
},
unblockPIN: function unblockPIN(puk, pin) {
return this._execPinCommand('unblock', puk, pin);
},
disablePIN: function disablePIN(pin) {
return this._execPinCommand('disable', pin);
},
changePIN: function changePIN(currentPin, newPin) {
return this._execPinCommand('change', currentPin, newPin);
},
};
exports.SIMAccess = SIMAccessManager;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,66 @@
'use strict';
/* exported ConfigSubView */
(function(exports) {
var ConfigSubView = function(id, crsAid, uiccAid, pinP2, defaultPin) {
this._id = id;
this._el = document.querySelector('#'+id);
this.crsInput = this._el.querySelector('#crs-aid');
this.crsInput.value = crsAid;
this.uiccInput = this._el.querySelector('#uicc-aid');
this.uiccInput.value = uiccAid;
this.pinP2Input = this._el.querySelector('#pinP2');
this.pinP2Input.value = pinP2;
this.defaultPinInput = this._el.querySelector('#default-pin');
this.defaultPinInput.value = defaultPin;
this._el.querySelector('header button.icon-back')
.addEventListener('click', () => this._handleConfigFinished());
this._el.querySelector('#settings-configuration-done')
.addEventListener('click', () => this._handleConfigFinished());
};
ConfigSubView.prototype = {
_el: null,
_id: null,
_visible: false,
crsInput: null,
uiccInput: null,
pinP2Input: null,
defaultPinInput: null,
get visible() {
return this._visible;
},
set visible(value) {
if(value) {
this._visible = true;
this._el.classList.add('slide-in');
} else {
this._visible = false;
this._el.classList.remove('slide-in');
}
},
_handleConfigFinished: function() {
if(!this.crsInput.validity.valid || !this.uiccInput.validity.valid ||
!this.pinP2Input.validity.valid ||
!this.defaultPinInput.validity.valid) {
this.debug('Inputs not valid, not closing settings');
return;
}
this.visible = false;
},
};
exports.ConfigSubView = ConfigSubView;
}(window));

View file

@ -0,0 +1,35 @@
'use strict';
/* globals AID */
/* exported DEFAULTS, SIM_REFRESH, SIM_UPDATE */
(function(exports) {
const DEFAULTS = {
PIN: false,
FASTPAY: true,
CRS_AID: AID.CRS,
UICC_AID: AID.UICC,
PIN_P2: '0E',
PIN_VALUE: '0000'
};
// settings properties which trigger sim data refresh on change
const SIM_REFRESH = ['crsAid'];
// settings properties which trigger SIMAccessManager update
const SIM_UPDATE = ['crsAid', 'uiccAid', 'pinP2'];
const PIN_DEFAULTS = {
MAX_PIN_ATTEMPTS: 3,
MAX_PUK_ATTEMPTS: 10,
PIN_LEN: 4,
MAX_SIM_PIN_LEN: 8,
PIN_PADDING: 255
};
exports.DEFAULTS = DEFAULTS;
exports.SIM_REFRESH = SIM_REFRESH;
exports.SIM_UPDATE = SIM_UPDATE;
exports.PIN_DEFAULTS = PIN_DEFAULTS;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,196 @@
'use strict';
/* globals addMixin, DebugMixin, ObserverSubjectMixin, SettingsView, DEFAULTS */
/* exported SettingsController */
(function(exports) {
var SettingsController = function(id, viewId, pinCtrl) {
this._id = id;
this._pinCtrl = pinCtrl;
addMixin(this, DebugMixin);
addMixin(this, ObserverSubjectMixin);
this._init(viewId);
};
SettingsController.prototype = {
_id: null,
_view: null,
_settingsData: {
'pin': DEFAULTS.PIN,
'fastPay': DEFAULTS.FASTPAY,
'crsAid': DEFAULTS.CRS_AID,
'uiccAid': DEFAULTS.UICC_AID,
'pinP2': DEFAULTS.PIN_P2,
'defaultPin': DEFAULTS.PIN_VALUE
},
get pinEnabled() {
return this._settingsData.pin;
},
get fastpayEnabled() {
return this._settingsData.fastPay;
},
get crsAid() {
return this._settingsData.crsAid;
},
get uiccAid() {
return this._settingsData.uiccAid;
},
get pinP2() {
return this._settingsData.pinP2;
},
get defaultPin() {
return this._settingsData.defaultPin;
},
show: function() {
this._view.visible = true;
},
hide: function() {
this._view.visible = false;
},
_init: function(viewId) {
Object.keys(this._settingsData).forEach(s => {
this._settingsData[s] = this._readSetting(s, this._settingsData[s]);
});
this._view = new SettingsView(viewId, Object.create(this._settingsData));
this._view.addListener(this);
this._view.setSubViewContent('settings-pin', this._pinCtrl.view);
},
_readSetting: function(settingName, defValue) {
var settingVal = localStorage.getItem(settingName);
if(settingVal === 'true' || settingVal === 'false') {
settingVal = (settingVal === 'true');
}
if(settingVal === null && defValue !== undefined) {
localStorage.setItem(settingName, defValue);
settingVal = defValue;
}
return settingVal;
},
onEvent: function(id, event) {
this.debug('Event from: ' + id + ', ' + JSON.stringify(event));
switch(event.action) {
case 'editing-finished': this._handleInput(event.data); break;
case 'pin-toggle': this._handlePinToggle(event.data.checked); break;
case 'pin-change': this._handlePinChange(); break;
case 'pin-cancel': this._handlePinCancel(); break;
}
},
addPinComponent: function(pinCtrl) {
this._pinCtrl = pinCtrl;
this._view.setSubViewContent('settings-pin', this._pinCtrl.view);
},
_handlePinToggle: function(enable) {
this._view.showSubview('settings-pin');
if(enable) {
this._handlePinEnable();
} else {
this._handlePinDisable();
}
},
_handlePinEnable: function() {
this._pinCtrl.enablePin()
.then((enabled) => {
this._view.pinEnabled = enabled;
if(enabled) {
this._view.hideSubview('settings-pin');
this._pin = true;
localStorage.setItem('pin', this._pin);
this._view.showStatus('Wallet PIN enabled successfully');
} else {
this.debug('PIN enable failure');
this._view.showStatus('Failed to enable Wallet PIN');
}
})
.catch(e => {
if(e !== 'pin-cancelled') {
this._view.hideSubview('settings-pin');
this._notify({ error: e, msg: 'PIN enable error' });
}
});
},
_handlePinDisable: function() {
this._pinCtrl.disablePin()
.then((disabled) => {
this._view.hideSubview('settings-pin');
this._view.pinEnabled = !disabled;
if(disabled) {
this._pin = false;
localStorage.setItem('pin', this._pin);
this._view.showStatus('Wallet PIN disabled successfully');
} else {
this.debug('PIN disable failure');
this._view.showStatus('Failed to disable Wallet PIN');
}
})
.catch(e => {
if(e !== 'pin-cancelled') {
this._view.hideSubview('settings-pin');
this._notify({ error: e, msg: 'PIN disable error' });
}
});
},
_handlePinChange: function() {
this._view.showSubview('settings-pin');
this._pinCtrl.changePin()
.then((success) => {
this._view.hideSubview('settings-pin');
if(success) {
this._view.showStatus('Wallet PIN changed successfully');
} else {
this.debug('Change failed');
this._view.showStatus('Failed to change Wallet PIN');
}
})
.catch(e => {
if(e !== 'pin-cancelled') {
this._view.hideSubview('settings-pin');
this._notify({ error: e, msg: 'PIN chnage error' });
}
});
},
_handlePinCancel() {
this._pinCtrl.cancelPinAction();
},
_handleInput: function(data) {
var changes = {};
Object.keys(data).forEach(s => {
if(this._settingsData[s] !== data[s]) {
this._settingsData[s] = data[s];
changes[s] = data[s];
localStorage.setItem(s, data[s]);
}
});
if(Object.keys(changes).length) {
this._notify({ action: 'settings-updated', changes: changes });
}
},
};
exports.SettingsController = SettingsController;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,49 @@
'use strict';
/* exported SettingsSubView */
(function(exports) {
var SettingsSubView = function(id, onCloseCb) {
this._id = id;
this._onCloseCallback = onCloseCb;
this._el = document.querySelector('#'+id);
this._el.querySelector('header button.icon-back')
.addEventListener('click', () => {
this.visible = false;
if(this._onCloseCallback) {
this._onCloseCallback();
}
});
};
SettingsSubView.prototype = {
_el: null,
_id: null,
_visible: false,
_onCloseCallback: null,
get visible() {
return this._visible;
},
set visible(value) {
if(value) {
this._visible = true;
this._el.classList.add('slide-in');
} else {
this._visible = false;
this._el.classList.remove('slide-in');
}
},
set content(el) {
var contentEl = this._el.querySelector('.subview-content');
this._el.replaceChild(el, contentEl);
el.classList.add('subview-content');
},
};
exports.SettingsSubView = SettingsSubView;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1,152 @@
'use strict';
/* globals addMixin, ObserverSubjectMixin, DebugMixin, SettingsSubView,
ConfigSubView */
/* exported SettingsView */
(function(exports) {
const STATUS_TIMEOUT = 4000; // ms
var SettingsView = function(id, options) {
this._id = id;
this._el = document.querySelector('#' + id);
this._initMainView(options.pin, options.fastPay);
this._initSubViews(options);
addMixin(this, ObserverSubjectMixin);
addMixin(this, DebugMixin);
};
SettingsView.prototype = {
_el: null,
_id: null,
_visible: false,
_subviews: [],
_pinCheckbox: null,
_fastPayCheckbox: null,
_pinChangeBtn: null,
_pinEnabled: false,
get visible() {
return this._visible;
},
set visible(value) {
if(value) {
this._visible = true;
this._el.classList.remove('hide');
this._el.classList.add('edit');
} else {
this._subviews.forEach((v) => { v.visible = false; });
this._visible = false;
this._el.classList.remove('edit');
}
},
set pinEnabled(value) {
this._pinCheckbox.checked = value;
if(value) {
this._pinChangeBtn.classList.remove('settings-disabled');
} else {
this._pinChangeBtn.classList.add('settings-disabled');
}
},
_initMainView: function(pin, fastPay) {
this._pinCheckbox = this._el.querySelector('#pin-checkbox');
this._pinCheckbox.addEventListener('change', () => {
this._notify({
action: 'pin-toggle',
data: { checked: this._pinCheckbox.checked }
});
this.pinEnabled = !this._pinCheckbox.checked;
});
this._pinChangeBtn = this._el.querySelector('#pin-change-btn');
this._pinChangeBtn.addEventListener('click', () => {
this._notify({ action: 'pin-change' });
});
this.pinEnabled = pin;
this._fastPayCheckbox = this._el.querySelector('#fast-pay');
this._fastPayCheckbox.checked = fastPay;
this._el.querySelector('#settings-done').addEventListener('click',
() => this._handleEditingFinished());
},
_initSubViews: function(cfg){
var config = new ConfigSubView('settings-configuration', cfg.crsAid,
cfg.uiccAid, cfg.pinP2, cfg.defaultPin);
this._el.querySelector('#adv-settings-btn')
.addEventListener('click', () => { config.visible = true; });
var about = new SettingsSubView('settings-about');
this._el.querySelector('#about-btn')
.addEventListener('click', () => { about.visible = true; });
var pinCloseHandler = () => this._handlePinViewClosed();
var pin = new SettingsSubView('settings-pin', pinCloseHandler);
this._subviews.push(config);
this._subviews.push(about);
this._subviews.push(pin);
},
_getSubView: function(id) {
return this._subviews.find(v => v._id === id);
},
setSubViewContent: function(id, content) {
var subView = this._getSubView(id);
if(subView) {
subView.content = content;
}
},
showSubview: function(id, hide) {
var subView = this._getSubView(id);
if(subView) {
subView.visible = !hide;
}
},
hideSubview: function(id) {
this.showSubview(id, true);
},
showStatus: function(statusMsg) {
this._el.querySelector('#settings-status p').textContent = statusMsg;
var status = this._el.querySelector('#settings-status');
status.classList.add('visible');
setTimeout(() => status.classList.remove('visible'), STATUS_TIMEOUT);
},
_handleEditingFinished: function _handleEditingFinished() {
var conf = this._getSubView('settings-configuration');
this._notify({
action: 'editing-finished',
data: {
pin: this._pinCheckbox.checked,
fastPay: this._fastPayCheckbox.checked,
crsAid: conf.crsInput.value,
uiccAid: conf.uiccInput.value,
pinP2: conf.pinP2Input.value,
defaultPin: conf.defaultPinInput.value
}
});
this.visible = false;
},
_handlePinViewClosed: function _handlePinViewClosed() {
this._notify({ action: 'pin-cancel' });
}
};
exports.SettingsView = SettingsView;
}((typeof exports === 'undefined') ? window : exports));

View file

@ -0,0 +1 @@
{"version":"2.5","name":"Mobile Wallet","description":"New wallet version for FFOS 2.2","launch_path":"/index.html","icons":{"84":"/img/icons/default_84.png","126":"/img/icons/default_126.png","142":"/img/icons/default_142.png","189":"/img/icons/default_189.png","284":"/img/icons/default_284.png"},"type":"certified","permissions":{"secureelement-manage":{},"nfc-hci-events":{}},"secure_element_access":["*/A0*"],"messages":[{"nfc-hci-event-transaction":"/index.html"}],"orientation":"portrait","default_locale":"en","origin":"app://mobile-wallet.gaiamobile.org"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 B

View file

@ -0,0 +1,151 @@
'use strict';
/* global ComponentUtils */
window.GaiaSwitch = (function(win) {
// Extend from the HTMLElement prototype
var proto = Object.create(HTMLElement.prototype);
// Allow baseurl to be overridden (used for demo page)
var baseurl = window.GaiaSwitchBaseurl ||
'/shared/elements/gaia_switch/';
proto.createdCallback = function() {
var shadow = this.createShadowRoot();
this._template = template.content.cloneNode(true);
this._input = this._template.querySelector('input[type="checkbox"]');
var checked = this.getAttribute('checked');
if (checked !== null) {
this._input.checked = true;
}
var wrapper = this._template.getElementById('switch');
wrapper.addEventListener('click', this.handleClick.bind(this));
shadow.appendChild(this._template);
ComponentUtils.style.call(this, baseurl);
// Proxy RTL changes to the shadow root so we can style for RTL.
var dirObserver = new MutationObserver(this.updateInternalDir.bind(this));
dirObserver.observe(document.documentElement, {
attributeFilter: ['dir'],
attributes: true
});
this.updateInternalDir();
};
proto.updateInternalDir = function() {
var internal = this.shadowRoot.firstElementChild;
if (document.documentElement.dir === 'rtl') {
internal.setAttribute('dir', 'rtl');
} else {
internal.removeAttribute('dir');
}
};
proto.handleClick = function(e) {
// If user clicked the (optional) link, do not toggle gaia-switch.
if (e && e.target.tagName === 'A') {
return;
}
e && e.preventDefault();
e && e.stopImmediatePropagation();
// Dispatch a click event.
var event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
this.dispatchEvent(event);
if (!event.defaultPrevented) {
this.checked = !this.checked;
}
// Dispatch a change event for the gaia-switch.
this.dispatchEvent(new CustomEvent('change', {
bubbles: true,
cancelable: false
}));
};
/**
* Allows users to simulate clicking through javascript.
*/
proto.click = function() {
this.handleClick();
};
/**
* Proxy the checked property to the input element.
*/
Object.defineProperty( proto, 'checked', {
get: function() {
return this._input.checked;
},
set: function(value) {
this._input.checked = value;
}
});
/**
* Disabled setter
*/
Object.defineProperty( proto, 'disabled', {
get: function() {
return this.hasAttribute('disabled');
},
set: function(value) {
if (value) {
this.setAttribute('disabled', true);
} else {
this.removeAttribute('disabled');
}
}
});
/**
* Proxy the name property to the input element.
*/
Object.defineProperty( proto, 'name', {
get: function() {
return this.getAttribute('name');
},
set: function(value) {
this.setAttribute('name', value);
}
});
/**
* Proxy the input type.
*/
Object.defineProperty( proto, 'type', {
get: function() {
return 'gaia-switch';
}
});
// HACK: Create a <template> in memory at runtime.
// When the custom-element is created we clone
// this template and inject into the shadow-root.
// Prior to this we would have had to copy/paste
// the template into the <head> of every app that
// wanted to use <gaia-switch>, this would make
// markup changes complicated, and could lead to
// things getting out of sync. This is a short-term
// hack until we can import entire custom-elements
// using HTML Imports (bug 877072).
var template = document.createElement('template');
template.innerHTML = `<span id="switch">
<input type="checkbox">
<span><content select="label"></content></span>
<div class="details"><content select="details"></content></div>
<content select="a"></content>
</span>`;
// Register and return the constructor
return document.registerElement('gaia-switch', { prototype: proto });
})(window);

View file

@ -0,0 +1,116 @@
gaia-switch {
display: inline-block;
width: 100%;
}
#switch {
display: inline-block;
vertical-align: middle;
width: 100%;
position: relative;
background: none;
}
#switch span {
font-size: 1.9rem;
color: var(--label-color);
padding: 1rem 0;
line-height: 3rem;
box-sizing: border-box;
display: block;
/* Override inheriting nowrap from legacy lists building blocks. */
white-space: normal;
}
#switch span:-moz-dir(ltr) { float: left; }
#switch span:-moz-dir(rtl) { float: right; }
#switch span:not(:empty) {
padding-inline-end: 6rem;
}
#switch input {
margin: 0;
opacity: 0;
position: absolute;
top: 0;
offset-inline-start: 0;
}
#switch input:checked ~ span::after {
background-position: center bottom;
}
/* ----------------------------------
* ON/OFF SWITCHES
* ---------------------------------- */
#switch input ~ span::after {
content: '';
position: absolute;
offset-inline-end: 0;
top: 2.5rem;
width: 6rem;
margin: -1.4rem 0 0;
height: 2.7rem;
border-radius: 1.35rem;
overflow: hidden;
background-color: #e6e6e6;
background-image: url(images/background_off.png);
background-repeat: no-repeat;
background-size: 9.2rem 2.7rem;
transition: background 0.2s ease;
}
#switch input ~ span:-moz-dir(ltr)::after { background-position: -3.2rem 0; }
#switch input ~ span:-moz-dir(rtl)::after { background-position: 0 0; }
/* switch: 'ON' state */
#switch input:checked ~ span:-moz-dir(ltr)::after {
background-image: url(images/background.png);
background-position: 0 0;
}
#switch input:checked ~ span:-moz-dir(rtl)::after {
background-image: url(images/background_rtl.png);
background-position: -3.2rem 0;
}
/* switch: disabled state */
#switch input:disabled ~ span::after {
opacity: 0.4;
}
#switch input.uninit ~ span::after {
transition: none;
}
/**
* A <details> slot may be specified in addition the the <label>.
* When provided, it appears under the <label> and can provide additional context.
*/
#switch div.details {
clear: both;
position: relative;
top: -1.5rem;
display: block;
margin: 0;
padding-top: 0.5rem;
padding-inline-start: 0;
padding-inline-end: 8rem;
font-size: 1.4rem;
line-height: 1.6rem;
color: #505859;
font-style: normal;
white-space: normal;
}
/**
* A <a> link slow may be specified in addition to the <label> and <details>.
* When provided, it will appear under the <details> section.
*/
gaia-switch a {
position: relative;
top: -0.8rem;
font-size: 1.7rem;
line-height: 2.4rem;
font-weight: 400;
color: #35679a;
}

View file

@ -0,0 +1,45 @@
(function(exports) {
'use strict';
/**
* ComponentUtils is a utility which allows us to use web components earlier
* than we should be able to by polyfilling and fixing platform deficiencies.
*/
exports.ComponentUtils = {
/**
* Injects a style.css into both the shadow root and outside the shadow
* root so we can style projected content. Bug 992249.
*/
style: function(baseUrl) { if (window.VulpesCompat) { window.VulpesCompat.componentStyle(this, baseUrl).catch(console.error); return; }
var style = document.createElement('style');
var url = baseUrl + 'style.css';
var self = this;
style.setAttribute('scoped', '');
style.innerHTML = '@import url(' + url + ');';
this.appendChild(style);
this.style.visibility = 'hidden';
// Wait for the stylesheet to load before injecting
// it into the shadow-dom. This is to work around
// bug 1003294, let's review once landed.
style.addEventListener('load', function() {
// Put a clone of the stylesheet into the shadow-dom.
// We have to use two <style> nodes, to work around
// the lack of `:host` (bug 992245) and `:content`
// (bug 992249) selectors. Once we have those we
// can do all our styling from a single style-sheet
// within the shadow-dom.
if (self.shadowRoot) {
self.shadowRoot.appendChild(style.cloneNode(true));
}
self.style.visibility = '';
});
}
};
}(window));

View file

@ -0,0 +1,684 @@
/* exported GestureDetector */
'use strict';
/**
* GestureDetector.js: generate events for one and two finger gestures.
*
* A GestureDetector object listens for touch events on a specified
* element and generates higher-level events that describe one and two finger
* gestures on the element.
*
* Supported events:
*
* tap like a click event
* dbltap like dblclick
* pan one finger motion
* swipe when a finger is released following pan events
* holdstart touch and hold. Must set an option to get these.
* holdmove motion after a holdstart event
* holdend when the finger goes up after holdstart/holdmove
* transform 2-finger pinch and twist gestures for scaling and rotation
*
* Each of these events is a bubbling CustomEvent with important details in the
* event.detail field. The event details are not yet stable and are not yet
* documented. See the calls to emitEvent() for details.
*
* To use this library, create a GestureDetector object by passing an element to
* the GestureDetector() constructor and then calling startDetecting() on it.
* The element will be the target of all the emitted gesture events. You can
* also pass an optional object as the second constructor argument. If you're
* interested in holdstart/holdmove/holdend events, pass {holdEvents:true} as
* this second argument. Otherwise they will not be generated.
* If you want to customize the pan threshold, pass {panThreshold:X}
* (X and Y in pixels) in the options argument.
*
* Implementation note: event processing is done with a simple finite-state
* machine. This means that in general, the various kinds of gestures are
* mutually exclusive. You won't get pan events until your finger has
* moved more than a minimum threshold, for example, but it does, the FSM enters
* a new state in which it can emit pan and swipe events and cannot emit hold
* events. Similarly, if you've started a 1 finger pan/swipe gesture and
* accidentally touch with a second finger, you'll continue to get pan events,
* and won't suddenly start getting 2-finger transform events.
*
* This library never calls preventDefault() or stopPropagation on any of the
* events it processes, so the raw touch events should still be
* available for other code to process. It is not clear to me whether this is a
* feature or a bug.
*/
var GestureDetector = (function() {
//
// Constructor
//
function GD(e, options) {
this.element = e;
this.options = options || {};
this.options.panThreshold = this.options.panThreshold || GD.PAN_THRESHOLD;
this.state = initialState;
this.timers = {};
}
//
// Public methods
//
GD.prototype.startDetecting = function() {
var self = this;
eventtypes.forEach(function(t) {
self.element.addEventListener(t, self);
});
};
GD.prototype.stopDetecting = function() {
var self = this;
eventtypes.forEach(function(t) {
self.element.removeEventListener(t, self);
});
};
//
// Internal methods
//
GD.prototype.handleEvent = function(e) {
var handler = this.state[e.type];
if (!handler) {
return;
}
// If this is a touch event handle each changed touch separately
if (e.changedTouches) {
for (var i = 0; i < e.changedTouches.length; i++) {
handler(this, e, e.changedTouches[i]);
// The first changed touch might have changed the state of the FSM.
handler = this.state[e.type];
if (!handler) {
return;
}
}
}
else { // Otherwise, just dispatch the event to the handler
handler(this, e);
}
};
GD.prototype.startTimer = function(type, time) {
this.clearTimer(type);
var self = this;
this.timers[type] = setTimeout(function() {
self.timers[type] = null;
var handler = self.state[type];
if (handler) {
handler(self, type);
}
}, time);
};
GD.prototype.clearTimer = function(type) {
if (this.timers[type]) {
clearTimeout(this.timers[type]);
this.timers[type] = null;
}
};
// Switch to a new FSM state, and call the init() function of that
// state, if it has one. The event and touch arguments are optional
// and are just passed through to the state init function.
GD.prototype.switchTo = function(state, event, touch) {
this.state = state;
if (state.init) {
state.init(this, event, touch);
}
};
GD.prototype.emitEvent = function(type, detail) {
if (!this.target) {
console.error('Attempt to emit event with no target');
return;
}
var event = this.element.ownerDocument.createEvent('CustomEvent');
event.initCustomEvent(type, true, true, detail);
this.target.dispatchEvent(event);
};
//
// Tuneable parameters
//
GD.HOLD_INTERVAL = 1000; // Hold events after 1000 ms
GD.PAN_THRESHOLD = 20; // 20 pixels movement before touch panning
GD.DOUBLE_TAP_DISTANCE = 50;
GD.DOUBLE_TAP_TIME = 500;
GD.VELOCITY_SMOOTHING = 0.5;
// Don't start sending transform events until the gesture exceeds a threshold
GD.SCALE_THRESHOLD = 20; // pixels
GD.ROTATE_THRESHOLD = 22.5; // degrees
// For pans and zooms, we compute new starting coordinates that are part way
// between the initial event and the event that crossed the threshold so that
// the first event we send doesn't cause a big lurch. This constant must be
// between 0 and 1 and says how far along the line between the initial value
// and the new value we pick
GD.THRESHOLD_SMOOTHING = 0.9;
//
// Helpful shortcuts and utility functions
//
var abs = Math.abs, floor = Math.floor, sqrt = Math.sqrt, atan2 = Math.atan2;
var PI = Math.PI;
// The names of events that we need to register handlers for
var eventtypes = [
'touchstart',
'touchmove',
'touchend'
];
// Return the event's timestamp in ms
function eventTime(e) {
// In gecko, synthetic events seem to be in microseconds rather than ms.
// So if the timestamp is much larger than the current time, assue it is
// in microseconds and divide by 1000
var ts = e.timeStamp;
if (ts > 2 * Date.now()) {
return Math.floor(ts / 1000);
} else {
return ts;
}
}
// Return an object containg the space and time coordinates of
// and event and touch. We freeze the object to make it immutable so
// we can pass it in events and not worry about values being changed.
function coordinates(e, t) {
return Object.freeze({
screenX: t.screenX,
screenY: t.screenY,
clientX: t.clientX,
clientY: t.clientY,
timeStamp: eventTime(e)
});
}
// Like coordinates(), but return the midpoint between two touches
function midpoints(e, t1, t2) {
return Object.freeze({
screenX: floor((t1.screenX + t2.screenX) / 2),
screenY: floor((t1.screenY + t2.screenY) / 2),
clientX: floor((t1.clientX + t2.clientX) / 2),
clientY: floor((t1.clientY + t2.clientY) / 2),
timeStamp: eventTime(e)
});
}
// Given coordinates objects c1 and c2, return a new coordinates object
// representing a point and time along the line between those points.
// The position of the point is controlled by the THRESHOLD_SMOOTHING constant
function between(c1, c2) {
var r = GD.THRESHOLD_SMOOTHING;
return Object.freeze({
screenX: floor(c1.screenX + r * (c2.screenX - c1.screenX)),
screenY: floor(c1.screenY + r * (c2.screenY - c1.screenY)),
clientX: floor(c1.clientX + r * (c2.clientX - c1.clientX)),
clientY: floor(c1.clientY + r * (c2.clientY - c1.clientY)),
timeStamp: floor(c1.timeStamp + r * (c2.timeStamp - c1.timeStamp))
});
}
// Compute the distance between two touches
function touchDistance(t1, t2) {
var dx = t2.screenX - t1.screenX;
var dy = t2.screenY - t1.screenY;
return sqrt(dx * dx + dy * dy);
}
// Compute the direction (as an angle) of the line between two touches
// Returns a number d, -180 < d <= 180
function touchDirection(t1, t2) {
return atan2(t2.screenY - t1.screenY,
t2.screenX - t1.screenX) * 180 / PI;
}
// Compute the clockwise angle between direction d1 and direction d2.
// Returns an angle a -180 < a <= 180.
function touchRotation(d1, d2) {
var angle = d2 - d1;
if (angle > 180) {
angle -= 360;
} else if (angle <= -180) {
angle += 360;
}
return angle;
}
// Determine if two taps are close enough in time and space to
// trigger a dbltap event. The arguments are objects returned
// by the coordinates() function.
function isDoubleTap(lastTap, thisTap) {
var dx = abs(thisTap.screenX - lastTap.screenX);
var dy = abs(thisTap.screenY - lastTap.screenY);
var dt = thisTap.timeStamp - lastTap.timeStamp;
return (dx < GD.DOUBLE_TAP_DISTANCE &&
dy < GD.DOUBLE_TAP_DISTANCE &&
dt < GD.DOUBLE_TAP_TIME);
}
//
// The following objects are the states of our Finite State Machine
//
// In this state we're not processing any gestures, just waiting
// for an event to start a gesture and ignoring others
var initialState = {
name: 'initialState',
init: function(d, e, t) {
// When we enter or return to the initial state, clear
// the detector properties that were tracking gestures
// Don't clear d.lastTap here, though. We need it for dbltap events
d.target = null;
d.start = d.last = null;
d.touch1 = d.touch2 = null;
d.vx = d.vy = null;
d.startDistance = d.lastDistance = null;
d.startDirection = d.lastDirection = null;
d.lastMidpoint = null;
d.scaled = d.rotated = null;
// If we were invoked with an event and touch, then process it.
if (e && t && e.type === 'touchstart') {
initialState.touchstart(d, e, t);
}
},
// Switch to the touchstarted state and process the touch event there
touchstart: function(d, e, t) {
d.switchTo(touchStartedState, e, t);
}
};
// One finger is down but we haven't generated any event yet. We're
// waiting to see... If the finger goes up soon, its a tap. If the finger
// stays down and still, its a hold. If the finger moves its a pan/swipe.
// And if a second finger goes down, its a transform
var touchStartedState = {
name: 'touchStartedState',
init: function(d, e, t) {
// Remember the target of the event
d.target = e.target;
// Remember the id of the touch that started
d.touch1 = t.identifier;
// Get the coordinates of the touch
d.start = d.last = coordinates(e, t);
// Start a timer for a hold
// If we're doing hold events, start a timer for them
if (d.options.holdEvents) {
d.startTimer('holdtimeout', GD.HOLD_INTERVAL);
}
},
touchstart: function(d, e, t) {
// If another finger goes down in this state, then
// go to transform state to start 2-finger gestures.
d.clearTimer('holdtimeout');
if (e.touches.length > 1) { // verify that we have 2 fingers
d.switchTo(transformState, e, t);
}
else {
// If Gecko fails to deliver a touchend event to us (bug 1162771, e.g.)
// then we might get two touchstart events in a row and end up here
// when there is actually only one finger currently on the screen.
// In that case, we switch back to the initial state and process
// this touch from that state. (We don't switch directly to
// touchStartedState because we need to reset things in the
// initialState init() function.)
console.warn('Ignoring missing touchend event. See bug 1162771.');
d.switchTo(initialState, e, t);
}
},
touchmove: function(d, e, t) {
// Ignore any touches but the initial one
// This could happen if there was still a finger down after
// the end of a previous 2-finger gesture, e.g.
if (t.identifier !== d.touch1) {
return;
}
if (abs(t.screenX - d.start.screenX) > d.options.panThreshold ||
abs(t.screenY - d.start.screenY) > d.options.panThreshold) {
d.clearTimer('holdtimeout');
d.switchTo(panStartedState, e, t);
}
},
touchend: function(d, e, t) {
// Ignore any touches but the initial one
if (t.identifier !== d.touch1) {
return;
}
// If there was a previous tap that was close enough in time
// and space, then emit a 'dbltap' event
if (d.lastTap && isDoubleTap(d.lastTap, d.start)) {
d.emitEvent('tap', d.start);
d.emitEvent('dbltap', d.start);
// clear the lastTap property, so we don't get another one
d.lastTap = null;
}
else {
// Emit a 'tap' event using the starting coordinates
// as the event details
d.emitEvent('tap', d.start);
// Remember the coordinates of this tap so we can detect double taps
d.lastTap = coordinates(e, t);
}
// In either case clear the timer and go back to the initial state
d.clearTimer('holdtimeout');
d.switchTo(initialState);
},
holdtimeout: function(d) {
d.switchTo(holdState);
}
};
// A single touch has moved enough to exceed the pan threshold and now
// we're going to generate pan events after each move and a swipe event
// when the touch ends. We ignore any other touches that occur while this
// pan/swipe gesture is in progress.
var panStartedState = {
name: 'panStartedState',
init: function(d, e, t) {
// Panning doesn't start until the touch has moved more than a
// certain threshold. But we don't want the pan to have a jerky
// start where the first event is a big distance. So proceed as
// pan actually started at a point along the path between the
// first touch and this current touch.
d.start = d.last = between(d.start, coordinates(e, t));
// If we transition into this state with a touchmove event,
// then process it with that handler. If we don't do this then
// we can end up with swipe events that don't know their velocity
if (e.type === 'touchmove') {
panStartedState.touchmove(d, e, t);
}
},
touchmove: function(d, e, t) {
// Ignore any fingers other than the one we're tracking
if (t.identifier !== d.touch1) {
return;
}
// Each time the touch moves, emit a pan event but stay in this state
var current = coordinates(e, t);
d.emitEvent('pan', {
absolute: {
dx: current.screenX - d.start.screenX,
dy: current.screenY - d.start.screenY
},
relative: {
dx: current.screenX - d.last.screenX,
dy: current.screenY - d.last.screenY
},
position: current
});
// Track the pan velocity so we can report this with the swipe
// Use a exponential moving average for a bit of smoothing
// on the velocity
var dt = current.timeStamp - d.last.timeStamp;
var vx = (current.screenX - d.last.screenX) / dt;
var vy = (current.screenY - d.last.screenY) / dt;
if (d.vx == null) { // first time; no average
d.vx = vx;
d.vy = vy;
}
else {
d.vx = d.vx * GD.VELOCITY_SMOOTHING +
vx * (1 - GD.VELOCITY_SMOOTHING);
d.vy = d.vy * GD.VELOCITY_SMOOTHING +
vy * (1 - GD.VELOCITY_SMOOTHING);
}
d.last = current;
},
touchend: function(d, e, t) {
// Ignore any fingers other than the one we're tracking
if (t.identifier !== d.touch1) {
return;
}
// Emit a swipe event when the finger goes up.
// Report start and end point, dx, dy, dt, velocity and direction
var current = coordinates(e, t);
var dx = current.screenX - d.start.screenX;
var dy = current.screenY - d.start.screenY;
// angle is a positive number of degrees, starting at 0 on the
// positive x axis and increasing clockwise.
var angle = atan2(dy, dx) * 180 / PI;
if (angle < 0) {
angle += 360;
}
// Direction is 'right', 'down', 'left' or 'up'
var direction;
if (angle >= 315 || angle < 45) {
direction = 'right';
} else if (angle >= 45 && angle < 135) {
direction = 'down';
} else if (angle >= 135 && angle < 225) {
direction = 'left';
} else if (angle >= 225 && angle < 315) {
direction = 'up';
}
d.emitEvent('swipe', {
start: d.start,
end: current,
dx: dx,
dy: dy,
dt: e.timeStamp - d.start.timeStamp,
vx: d.vx,
vy: d.vy,
direction: direction,
angle: angle
});
// Go back to the initial state
d.switchTo(initialState);
}
};
// We enter this state if the user touches and holds for long enough
// without moving much. When we enter we emit a holdstart event. Motion
// after the holdstart generates holdmove events. And when the touch ends
// we generate a holdend event. holdmove and holdend events can be used
// kind of like drag and drop events in a mouse-based UI. Currently,
// these events just report the coordinates of the touch. Do we need
// other details?
var holdState = {
name: 'holdState',
init: function(d) {
d.emitEvent('holdstart', d.start);
},
touchmove: function(d, e, t) {
var current = coordinates(e, t);
d.emitEvent('holdmove', {
absolute: {
dx: current.screenX - d.start.screenX,
dy: current.screenY - d.start.screenY
},
relative: {
dx: current.screenX - d.last.screenX,
dy: current.screenY - d.last.screenY
},
position: current
});
d.last = current;
},
touchend: function(d, e, t) {
var current = coordinates(e, t);
d.emitEvent('holdend', {
start: d.start,
end: current,
dx: current.screenX - d.start.screenX,
dy: current.screenY - d.start.screenY
});
d.switchTo(initialState);
}
};
// We enter this state if a second touch starts before we start
// recoginzing any other gesture. As the touches move we track the
// distance and angle between them to report scale and rotation values
// in transform events.
var transformState = {
name: 'transformState',
init: function(d, e, t) {
// Remember the id of the second touch
d.touch2 = t.identifier;
// Get the two Touch objects
var t1 = e.touches.identifiedTouch(d.touch1);
var t2 = e.touches.identifiedTouch(d.touch2);
// Compute and remember the initial distance and angle
d.startDistance = d.lastDistance = touchDistance(t1, t2);
d.startDirection = d.lastDirection = touchDirection(t1, t2);
// Don't start emitting events until we're past a threshold
d.scaled = d.rotated = false;
},
touchmove: function(d, e, t) {
// Ignore touches we're not tracking
if (t.identifier !== d.touch1 && t.identifier !== d.touch2) {
return;
}
// Get the two Touch objects
var t1 = e.touches.identifiedTouch(d.touch1);
var t2 = e.touches.identifiedTouch(d.touch2);
// Compute the new midpoints, distance and direction
var midpoint = midpoints(e, t1, t2);
var distance = touchDistance(t1, t2);
var direction = touchDirection(t1, t2);
var rotation = touchRotation(d.startDirection, direction);
// Check all of these numbers against the thresholds. Otherwise
// the transforms are too jittery even when you try to hold your
// fingers still.
if (!d.scaled) {
if (abs(distance - d.startDistance) > GD.SCALE_THRESHOLD) {
d.scaled = true;
d.startDistance = d.lastDistance =
floor(d.startDistance +
GD.THRESHOLD_SMOOTHING * (distance - d.startDistance));
} else {
distance = d.startDistance;
}
}
if (!d.rotated) {
if (abs(rotation) > GD.ROTATE_THRESHOLD) {
d.rotated = true;
} else {
direction = d.startDirection;
}
}
// If nothing has exceeded the threshold yet, then we
// don't even have to fire an event.
if (d.scaled || d.rotated) {
// The detail field for the transform gesture event includes
// 'absolute' transformations against the initial values and
// 'relative' transformations against the values from the last
// transformgesture event.
d.emitEvent('transform', {
absolute: { // transform details since gesture start
scale: distance / d.startDistance,
rotate: touchRotation(d.startDirection, direction)
},
relative: { // transform since last gesture change
scale: distance / d.lastDistance,
rotate: touchRotation(d.lastDirection, direction)
},
midpoint: midpoint
});
d.lastDistance = distance;
d.lastDirection = direction;
d.lastMidpoint = midpoint;
}
},
touchend: function(d, e, t) {
// If either finger goes up, we're done with the gesture.
// The user might move that finger and put it right back down
// again to begin another 2-finger gesture, so we can't go
// back to the initial state while one of the fingers remains up.
// On the other hand, we can't go back to touchStartedState because
// that would mean that the finger left down could cause a tap or
// pan event. So we need an afterTransform state that waits for
// a finger to come back down or the other finger to go up.
if (t.identifier === d.touch2) {
d.touch2 = null;
} else if (t.identifier === d.touch1) {
d.touch1 = d.touch2;
d.touch2 = null;
} else {
return; // It was a touch we weren't tracking
}
// If we emitted any transform events, now we need to emit
// a transformend event to end the series. The details of this
// event use the values from the last touchmove, and the
// relative amounts will 1 and 0, but they are included for
// completeness even though they are not useful.
if (d.scaled || d.rotated) {
d.emitEvent('transformend', {
absolute: { // transform details since gesture start
scale: d.lastDistance / d.startDistance,
rotate: touchRotation(d.startDirection, d.lastDirection)
},
relative: { // nothing has changed relative to the last touchmove
scale: 1,
rotate: 0
},
midpoint: d.lastMidpoint
});
}
d.switchTo(afterTransformState);
}
};
// We did a tranform and one finger went up. Wait for that finger to
// come back down or the other finger to go up too.
var afterTransformState = {
name: 'afterTransformState',
touchstart: function(d, e, t) {
d.switchTo(transformState, e, t);
},
touchend: function(d, e, t) {
if (t.identifier === d.touch1) {
d.switchTo(initialState);
}
}
};
return GD;
}());

View file

@ -0,0 +1,428 @@
/* ----------------------------------
* Buttons
* ---------------------------------- */
.button::-moz-focus-inner,
.bb-button::-moz-focus-inner,
button::-moz-focus-inner {
border: none;
outline: none;
}
button,
.bb-button,
.button {
font-family: sans-serif;
font-style: italic;
width: 100%;
height: 4rem;
margin: 0 0 1rem;
padding: 0 1.2rem;
box-sizing: border-box;
display: inline-block;
vertical-align: middle;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
background: #d8d8d8;
border: none;
border-radius: 2rem;
font-weight: normal;
font-size: 1.6rem;
line-height: 4rem;
color: #333;
text-align: center;
text-decoration: none;
outline: none;
}
/* Recommend */
button.recommend,
.bb-button.recommend {
background-color: #00caf2;
color: #fff;
}
/* Danger */
button.danger,
.danger.bb-button {
background-color: #e51e1e;
color: #fff;
}
/* Pressed */
button:active,
.bb-button:active,
.button:active {
background-color: #b2f2ff;
color: #fff;
}
/* Icons */
button[data-icon],
.bb-button[data-icon],
.button[data-icon] {
position: relative;
}
button[data-icon]:before,
.bb-button[data-icon]:before,
.button[data-icon]:before {
position: absolute;
top: 50%;
margin-top: -1.5rem;
offset-inline-end: 1.5rem;
line-height: 3rem;
}
button[data-icon]:active:before,
.bb-button[data-icon]:active:before,
.button[data-icon]:active:before {
color: #fff;
}
/* Pressed with dark skin */
.skin-dark button:active,
.skin-dark .bb-button:active,
.skin-dark .button:active {
background-color: #00aacc;
}
/* Disabled */
button[disabled],
.bb-button[aria-disabled="true"],
.button[aria-disabled="true"] {
background-color: rgba(0,0,0,0.05);
color: #c7c7c7;
pointer-events: none;
}
button[disabled].recommend,
.bb-button[aria-disabled="true"].recommend,
.button[aria-disabled="true"].recommend {
background-color: #97cbd5;
color: rgba(255,255,255,0.5);
}
button[disabled].danger,
.bb-button[aria-disabled="true"].danger,
.button[aria-disabled="true"].danger {
background-color: #eb7474;
color: rgba(255,255,255,0.5);
}
/* Disabled with dark background */
.skin-dark button[disabled],
.skin-dark .button[aria-disabled="true"],
.skin-dark .bb-button[aria-disabled="true"] {
background-color: #565656;
color: rgba(255,255,255,0.4);
}
.skin-dark button[disabled].recommend,
.skin-dark .button[aria-disabled="true"].recommend,
.skin-dark .bb-button[aria-disabled="true"].recommend {
background-color: #006579;
}
.skin-dark button[disabled].danger,
.skin-dark .button[aria-disabled="true"].danger,
.skin-dark .bb-button[aria-disabled="true"].danger {
background-color: #730f0f;
}
button[disabled]::-moz-selection,
.button[aria-disabled="true"]::-moz-selection,
.bb-button[aria-disabled="true"]::-moz-selection {
-moz-user-select: none;
}
/* ----------------------------------
* Buttons inside lists
* ---------------------------------- */
li button,
li .bb-button,
li .button {
position: relative;
border: 0.1rem solid #c7c7c7;
background: rgba(0,0,0,0.05);
margin: 0 0 1rem;
overflow: hidden;
border-radius: 0;
display: flex;
height: auto;
min-height: 4rem;
text-overflow: initial;
white-space: normal;
line-height: 2rem;
padding: 1rem 1.2rem;
}
html[dir="ltr"] li button,
html[dir="ltr"] li .bb-button,
html[dir="ltr"] li .button {
text-align: left;
}
html[dir="rtl"] li button,
html[dir="rtl"] li .bb-button,
html[dir="rtl"] li .button {
text-align: right;
}
/* Press */
li .bb-button:active:after,
li .button:active:after,
li button:active:after {
opacity: 0;
}
/* Disabled */
li button:disabled,
li .bb-button[aria-disabled="true"],
li .button[aria-disabled="true"] {
opacity: 0.5;
}
/* Icons */
li button.icon,
li .bb-button.icon,
li .button.icon {
padding-inline-end: 4rem;
}
li button.icon:before,
li .bb-button.icon:before,
li .button.icon:before {
content: "";
width: 3rem;
height: 3rem;
position: absolute;
top: 50%;
offset-inline-end: 0.5rem;
margin-top: -1.5rem;
background: transparent no-repeat center center / 100% auto;
pointer-events: none;
}
li button[data-icon]:before,
li .bb-button[data-icon]:before,
li .button[data-icon]:before {
offset-inline-end: 1rem;
}
li button.icon-view:active,
li .bb-button.icon-view:active,
li .button.icon-view:active {
background-color: #b2f2ff;
}
li button[data-icon="expand"]:before,
li .bb-button[data-icon="expand"]:before,
li .button[data-icon="expand"]:before {
color: #00aac5;
top: unset;
bottom: 0;
}
li button[data-icon="expand"][disabled]:before,
li .bb-button[data-icon="expand"][aria-disabled="true"]:before,
li .button[data-icon="expand"][aria-disabled="true"]:before {
color: #A9A9A9;
}
li button[data-icon="forward"]:before,
li .bb-button[data-icon="forward"]:before,
li .button[data-icon="forward"]:before {
color: #686f72;
}
/* Getting more space for text, as icon is shorter */
li button.icon-dialog,
li .bb-button.icon-dialog,
li .button.icon-dialog {
padding-inline-end: 3rem;
}
li button.icon-dialog:before,
li .bb-button.icon-dialog:before,
li .button.icon-dialog:before {
width: 0;
height: 0;
top: auto;
offset-inline-end: 0.8rem;
bottom: 0.5rem;
border-top: 0.7rem solid transparent;
border-bottom: 0.7rem solid transparent;
border-inline-start: 0.7rem solid #00aac5;
z-index: 1;
}
html[dir="ltr"] li button.icon-dialog:before,
html[dir="ltr"] li .bb-button.icon-dialog:before,
html[dir="ltr"] li .button.icon-dialog:before {
transform: rotate(45deg);
}
html[dir="rtl"] li button.icon-dialog:before,
html[dir="rtl"] li .bb-button.icon-dialog:before,
html[dir="rtl"] li .button.icon-dialog:before {
transform: rotate(-45deg);
}
li button.icon-dialog:disabled:before,
li .bb-button[aria-disabled="true"].icon-dialog:before,
li .button[aria-disabled="true"].icon-dialog:before {
border-inline-start: 0.7rem solid #a9a9a9;
}
/* ----------------------------------
* Theme: Dark
* ---------------------------------- */
.skin-dark li button,
.skin-dark li .bb-button,
.skin-dark li .button {
background-color: #454545;
border: none;
color: #fff;
font-weight: normal;
}
.skin-dark li button:active,
.skin-dark li .button:active,
.skin-dark li .bb-button:active {
background-color: #00aacc;
}
.skin-dark li button.icon-view:active,
.skin-dark li .button.icon-view:active,
.skin-dark li .bb-button.icon-view:active {
background-color: #00aacc;
}
.skin-dark li button.icon-dialog:active:before,
.skin-dark li .bb-button.icon-dialog:active:before,
.skin-dark li .button.icon-dialog:active:before {
border-inline-start: 0.7rem solid #b2f2ff;
}
.skin-dark li button:disabled,
.skin-dark li .bb-button[aria-disabled="true"],
.skin-dark li .button[aria-disabled="true"] {
background-color: #454545;
opacity: 0.5;
}
/* Inputs inside of .button */
.button input,
body[role="application"] .bb-button input,
body[role="application"] .button input {
border: 0;
background: none;
}
/* select */
select {
width: 100%;
box-sizing: border-box;
font-family: sans-serif;
font-style: italic;
font-size: 1.6rem;
color: #333;
margin: 0 0 1rem;
padding: 0;
padding-inline-start: 1.1rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
height: 4rem;
border: 0.1rem solid #c7c7c7;
background: rgba(0,0,0,0.05);
}
.skin-dark select {
background-color: rgba(87, 87, 87, 0.8);
border: none;
color: #fff;
}
/* remove dotted outline */
select:-moz-focusring {
color: transparent;
text-shadow: 0 0 0 #333;
}
.skin-dark select:-moz-focusring {
text-shadow: 0 0 0 #fff;
}
/* Hides dropdown arrow until bug #649849 is fixed */
.bb-button.icon select,
.button.icon select,
.bb-button[data-icon] select,
.button[data-icon] select {
position: absolute;
top: -0.6rem;
offset-inline-start: 0;
width: calc(100% + 3rem);
height: 100%;
margin: 0;
padding: 0;
padding-inline-end: 5rem;
text-indent: 0;
font-weight: 400;
font-size: 1.7rem;
color: rgba(0,0,0,0);
border: none;
background: none;
}
/**
* The select element uses the text color to determine the color of the outline.
* We put a text shadow with needed offset and no blur, so that replaces the text.
*/
html[dir="ltr"] .bb-button.icon select,
html[dir="ltr"] .button.icon select,
html[dir="ltr"] .bb-button[data-icon] select,
html[dir="ltr"] .button[data-icon] select,
html[dir="ltr"] .bb-button.icon-dialog select,
html[dir="ltr"] .button.icon-dialog select {
text-shadow: 1rem 0.5rem 0 #333;
}
html[dir="rtl"] .bb-button.icon select,
html[dir="rtl"] .button.icon select,
html[dir="rtl"] .bb-button[data-icon] select,
html[dir="rtl"] .button[data-icon] select,
html[dir="rtl"] .bb-button.icon-dialog select,
html[dir="rtl"] .button.icon-dialog select {
text-shadow: -1rem 0.5rem 0 #333;
}
.bb-button.icon select option,
.button.icon select option,
.bb-button[data-icon] select option,
.button[data-icon] select option {
color: #000;
border: 0;
padding: 0.6rem 1.3rem;
text-shadow: 0 0 0 rgba(0,0,0,0);
}
.bb-button.icon-dialog,
.button.icon-dialog {
overflow: hidden;
}
html[dir="ltr"] .skin-dark .bb-button.icon select,
html[dir="ltr"] .skin-dark .button.icon select,
html[dir="ltr"] .skin-dark .bb-button[data-icon] select,
html[dir="ltr"] .skin-dark .button[data-icon] select,
html[dir="ltr"] .skin-dark .bb-button.icon-dialog select,
html[dir="ltr"] .skin-dark .button.icon-dialog select {
text-shadow: 1rem 0.5rem 0 #fff;
}
html[dir="rtl"] .skin-dark .bb-button.icon select,
html[dir="rtl"] .skin-dark .button.icon select,
html[dir="rtl"] .skin-dark .bb-button[data-icon] select,
html[dir="rtl"] .skin-dark .button[data-icon] select,
html[dir="rtl"] .skin-dark .bb-button.icon-dialog select,
html[dir="rtl"] .skin-dark .button.icon-dialog select {
text-shadow: -1rem 0.5rem 0 #fff;
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30"><path fill="#667174" fill-opacity=".7" d="m 18.2995,19.66 -5.056,-5.057 5.056,-5.056 c -0.31,-0.516 -0.62,-1.032 -0.93,-1.34 l -5.672,5.673 v 1.652 l 5.675,5.676 c 0.31,-0.515 0.722,-0.928 0.93,-1.547 z"/></svg>

After

Width:  |  Height:  |  Size: 294 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30"><path fill="#667174" fill-opacity=".7" d="m 11.698,19.66 5.056,-5.057 -5.056,-5.056 c 0.31,-0.516 0.62,-1.032 0.93,-1.34 L 18.3,13.88 v 1.652 l -5.675,5.676 c -0.31,-0.515 -0.722,-0.928 -0.93,-1.547 z"/></svg>

After

Width:  |  Height:  |  Size: 292 B

View file

@ -0,0 +1,280 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="utf-8">
<title>Buttons</title>
<link rel="stylesheet" href="../buttons.css">
<link rel="stylesheet" href="../../elements/gaia-icons/gaia-icons-embedded.css">
<link rel="stylesheet" href="../../elements/gaia-icons/bidi-helper.css">
<!--
- These <style> and <script> elements are only used for the example code,
- they are not needed for the real case.
-->
<style>
html, body {
margin: 0;
padding: 0;
font-size: 10px;
background-color: #fff;
}
label.bb-docs {
font-family: sans-serif;
font-size: 1.2rem;
margin-left: 1.5rem;
}
h2.bb-docs {
font-size: 1.8rem;
font-weight: 300;
color: #666;
margin: -0.1rem 0 0;
background-color: #f5f5f5;
padding: 0.4rem 3rem;
border: solid 0.1rem #e8e8e8;
font-family: sans-serif;
}
div.bb-docs {
padding: 1.5rem;
}
div.bb-docs.skin-organic {
background: #f4f4f4;
}
div.bb-docs.skin-dark {
background: #000;
}
ul {
padding: 0;
margin: 0;
list-style: none;
}
body[role="application"] > [role="region"] {
height: 100%;
position: relative;
}
[data-icon]:before {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
<script>
window.onload = function() {
document.getElementById('dir').checked = false;
};
</script>
</head>
<body role="application">
<p dir="ltr">
<label class="bb-docs">
<input id="dir" type="checkbox" checked="false"
onchange="document.documentElement.dir = this.checked ? 'rtl' : 'ltr'">
RTL mode
</label>
</p>
<h2 class="bb-docs">Special buttons</h2>
<div class="bb-docs skin-organic">
<button>Default</button>
<button class="recommend">Recommend</button>
<button class="danger">Danger</button>
</div>
<h2 class="bb-docs">Buttons with icons</h2>
<div class="bb-docs skin-organic">
<button data-icon="forward" aria-label="forward" data-l10n-id="">Default</button>
<button data-icon="forward" aria-label="forward" data-l10n-id="" class="recommend">Recommend</button>
<button data-icon="forward" aria-label="forward" data-l10n-id="" class="danger">Danger</button>
<button data-icon="forward-light" aria-label="forward-light" data-l10n-id="">Default</button>
<button data-icon="back" aria-label="back" data-l10n-id="">Default</button>
<button data-icon="back-light" aria-label="back-light" data-l10n-id="">Default</button>
</div>
<h2 class="bb-docs">Disabled</h2>
<div class="bb-docs skin-organic">
<button disabled>Default</button>
<a href="#" role="button" aria-disabled="true" class="recommend bb-button">Recommend</a>
<button disabled class="danger">Danger</button>
</div>
<h2 class="bb-docs">Skin dark</h2>
<div class="bb-docs skin-dark">
<button>Default</button>
<button class="recommend">Recommend</button>
<button class="danger">Danger</button>
</div>
<h2 class="bb-docs">Disabled</h2>
<div class="bb-docs skin-dark">
<button disabled>Default</button>
<a href="#" role="button" aria-disabled="true" class="recommend bb-button">Recommend</a>
<button disabled class="danger">Danger</button>
</div>
<h2 class="bb-docs">List buttons</h2>
<div class="bb-docs skin-organic">
<ul>
<li>
<button>Default</button>
</li>
<li>
<button disabled>Disabled</button>
</li>
<li>
<button class="icon icon-view">
Icon view button
</button>
</li>
<li>
<button class="icon icon-dialog">
Icon dialog button
</button>
</li>
<li>
<span class="button icon icon-dialog">
<select>
<option selected>Icon dialog with options</option>
<option>option 1</option>
<option>option 2</option>
<option>option 3</option>
</select>
</span>
</li>
<li>
<button class="icon icon-dialog" disabled>
Icon dialog button disabled
</button>
</li>
<li>
<button data-icon="forward" aria-label="forward" data-l10n-id="">
Side navigation
</button>
</li>
<li>
<button disabled data-icon="forward" aria-label="forward" data-l10n-id="">
Side navigation
</button>
</li>
<li>
<a role="button" data-icon="expand" class="bb-button" aria-label="expand" data-l10n-id="">
Tuesday Sept. 18, 2012
</a>
</li>
<li>
<a role="button" aria-disabled="true" data-icon="expand" class="bb-button" aria-label="expand" data-l10n-id="">
Tuesday Sept. 18, 2012
</a>
</li>
</ul>
</div>
<h2 class="bb-docs">Skin Dark</h2>
<div class="bb-docs skin-dark">
<ul>
<li>
<button>Default</button>
</li>
<li>
<button disabled>Disabled</button>
</li>
<li>
<button class="icon icon-dialog">
Dark icon dialog button
</button>
</li>
<li>
<button data-icon="forward" aria-label="forward" data-l10n-id="">
Side navigation
</button>
</li>
<li>
<button disabled data-icon="forward" aria-label="forward" data-l10n-id="">
Side navigation
</button>
</li>
<li>
<a role="button" data-icon="expand" class="bb-button" aria-label="expand" data-l10n-id="">
Tuesday Sept. 18, 2012
</a>
</li>
<li>
<a role="button" aria-disabled="true" data-icon="expand" class="bb-button" aria-label="expand" data-l10n-id="">
Tuesday Sept. 18, 2012
</a>
</li>
</ul>
</div>
<h2 class="bb-docs">Select</h2>
<div class="bb-docs">
<select>
<option>option 1</option>
<option>option 2</option>
</select>
</div>
<h2 class="bb-docs">Fake select</h2>
<div class="bb-docs">
<ul>
<li>
<span class="bb-button" data-icon="expand" aria-label="expand" data-l10n-id="">
<select>
<option>option 1</option>
<option>option 2</option>
</select>
</span>
</li>
</ul>
</div>
<h2 class="bb-docs">Disabled select</h2>
<div class="bb-docs">
<ul>
<li>
<span aria-disabled="true" class="bb-button" data-icon="expand" aria-label="expand" data-l10n-id="">
<select>
<option>option 1</option>
<option>option 2</option>
</select>
</span>
</li>
</ul>
</div>
<h2 class="bb-docs">Skin dark</h2>
<div class="bb-docs skin-dark">
<select>
<option>option 1</option>
<option>option 2</option>
</select>
</div>
<h2 class="bb-docs">Fake select</h2>
<div class="bb-docs skin-dark">
<ul>
<li>
<span class="bb-button" data-icon="expand" aria-label="expand" data-l10n-id="">
<select>
<option>option 1</option>
<option>option 2</option>
</select>
</span>
</li>
<li>
<span aria-disabled="true" class="bb-button" data-icon="expand" aria-label="expand" data-l10n-id="">
<select>
<option>option 1</option>
<option>option 2</option>
</select>
</span>
</li>
</ul>
</div>
</body>
</html>

View file

@ -0,0 +1,378 @@
/* -------------------------------------------------------------------
HEADERS: layout
Form factor or Skin dependent styles should NOT be defined here.
------------------------------------------------------------------- */
section[role="region"] > header:first-child {
position: relative;
display: block;
z-index: 10;
padding: 0;
height: 5rem;
border: none;
}
section[role="region"] > header:first-child h1 {
font-size: 2.3rem;
line-height: 5rem;
white-space: nowrap;
text-overflow: ellipsis;
display: block;
overflow: hidden;
margin: 0;
padding: 0 1rem;
height: 100%;
font-weight: 300;
font-style: italic;
text-align: center;
}
section[role="region"] > header:first-child h1 em {
font-weight: 400;
font-size: 1.5rem;
line-height: 1em;
font-style: normal;
}
section[role="region"] > header:first-child menu {
height: 100%;
padding: 0;
margin: 0;
}
html[dir="ltr"] section[role="region"] > header:first-child menu {
float: right;
}
html[dir="rtl"] section[role="region"] > header:first-child menu {
float: left;
}
/* ----------------------------------
Buttons
---------------------------------- */
section[role="region"] > header:first-child a,
section[role="region"] > header:first-child button {
box-sizing: border-box;
position: relative;
display: block;
overflow: hidden;
min-width: 5rem;
width: auto;
height: 5rem;
border: none;
background: none;
padding: 0 1rem;
margin: 0;
font-weight: 400;
font-size: 1.5rem;
line-height: 5rem;
border-radius: 0;
text-decoration: none;
text-align: center;
text-shadow: none;
font-style: italic;
}
html[dir="ltr"] section[role="region"] > header:first-child a,
html[dir="ltr"] section[role="region"] > header:first-child button {
float: left;
}
html[dir="rtl"] section[role="region"] > header:first-child a,
html[dir="rtl"] section[role="region"] > header:first-child button {
float: right;
}
/* Pressed state */
section[role="region"] > header:first-child a:focus {
outline: none;
}
section[role="region"] > header:first-child button::-moz-focus-inner {
outline: none;
border: none;
margin-top: -0.2rem; /* To fix line-height bug (697451) */
padding: 0;
}
/* Disabled state */
section[role="region"] > header:first-child a[aria-disabled="true"],
section[role="region"] > header:first-child button[disabled] {
opacity: 0.3;
pointer-events: none;
}
/* ----------------------------------
Icons
---------------------------------- */
section[role="region"] > header:first-child .icon {
display: block;
width: 3rem;
height: 5rem;
background: transparent no-repeat center / 3rem auto;
font-size: 0;
}
/* ----------------------------------
Subheader
---------------------------------- */
section[role="region"] > header {
z-index: 0;
height: auto;
}
section[role="region"] header h2 {
margin: 0;
padding: 0.8rem 3rem;
font-weight: 400;
font-size: 1.5rem;
line-height: 1.8rem;
}
/* -----------------------------------------------------------------
HEADER SKIN: default
Default values in case we are not overriding them using
class="skin-*"
----------------------------------------------------------------- */
section[role="region"] > header:first-child {
color: #fff;
background-color: #f97c17;
}
section[role="region"] > header:first-child h1 {
color: #fff;
}
section[role="region"] > header:first-child a,
section[role="region"] > header:first-child button {
color: rgba(0,0,0,0.6);
}
section[role="region"] > header:first-child a:active,
section[role="region"] > header:first-child button:active,
section[role="region"] > header:first-child a:hover,
section[role="region"] > header:first-child button:hover {
background-color: rgba(0,0,0,0.07);
}
section[role="region"] > header:first-child a:focus,
section[role="region"] > header:first-child button:focus {
background-color: transparent;
}
/* ----------------------------------
Icons
---------------------------------- */
section[role="region"] > header:first-child .icon-add {
background-image: url(headers/images/icons/add.png);
}
section[role="region"] > header:first-child .icon-compose {
background-image: url(headers/images/icons/compose.png);
}
html[dir="ltr"] section[role="region"] > header:first-child .icon-edit {
background-image: url(headers/images/icons/edit.png);
}
html[dir="rtl"] section[role="region"] > header:first-child .icon-edit {
background-image: url(headers/images/icons/edit-rtl.png);
}
html[dir="ltr"] section[role="region"] > header:first-child .icon-send {
background-image: url(headers/images/icons/send.png);
}
html[dir="rtl"] section[role="region"] > header:first-child .icon-send {
background-image: url(headers/images/icons/send_rtl.png);
}
section[role="region"] > header:first-child .icon-close {
background-image: url(headers/images/icons/close.png);
}
html[dir="ltr"] section[role="region"] > header:first-child .icon-back {
background-image: url(headers/images/icons/left.png);
}
html[dir="rtl"] section[role="region"] > header:first-child .icon-back {
background-image: url(headers/images/icons/right.png);
}
section[role="region"] > header:first-child .icon-menu {
background-image: url(headers/images/icons/menu.png);
}
section[role="region"] > header:first-child .icon-user {
background-image: url(headers/images/icons/user.png);
}
section[role="region"] > header:first-child .icon-up {
background-image: url(headers/images/icons/up.png);
}
section[role="region"] > header:first-child .icon-down {
background-image: url(headers/images/icons/down.png);
}
section[role="region"] > header:first-child .icon-options {
background-image: url(headers/images/icons/options.png);
}
/* ----------------------------------
Subheader
---------------------------------- */
section[role="region"] header h2 {
color: #424242;
border-bottom: solid 0.1rem #e6e6e6;
}
/* -----------------------------------------------------------------
HEADER SKIN: comms
.skin-comms will override default values
----------------------------------------------------------------- */
.skin-comms section[role="region"] > header:first-child,
section[role="region"].skin-comms > header:first-child {
background-color: #00adad;
}
.skin-comms section[role="region"] > header:first-child a,
.skin-comms section[role="region"] > header:first-child button,
section[role="region"].skin-comms > header:first-child a,
section[role="region"].skin-comms > header:first-child button {
color: rgba(0,0,0,0.5);
}
.skin-comms section[role="region"] > header:first-child a:active,
.skin-comms section[role="region"] > header:first-child button:active,
.skin-comms section[role="region"] > header:first-child a:hover,
.skin-comms section[role="region"] > header:first-child button:hover,
section[role="region"].skin-comms > header:first-child a:active,
section[role="region"].skin-comms > header:first-child button:active,
section[role="region"].skin-comms > header:first-child a:hover,
section[role="region"].skin-comms > header:first-child button:hover {
background-color: rgba(0,0,0,0.1);
}
.skin-comms section[role="region"] > header:first-child a:focus,
.skin-comms section[role="region"] > header:first-child button:focus,
section[role="region"].skin-comms > header:first-child a:focus,
section[role="region"].skin-comms > header:first-child button:focus {
background-color: transparent;
}
/* -----------------------------------------------------------------
HEADER SKIN: dark
.skin-dark will override default values
----------------------------------------------------------------- */
section[role="region"].skin-dark > header:first-child,
.skin-dark > section[role="region"] > header:first-child {
background-color: #242d33;
}
.skin-dark section[role="region"] > header:first-child a,
.skin-dark section[role="region"] > header:first-child button,
section[role="region"].skin-dark > header:first-child a,
section[role="region"].skin-dark > header:first-child button {
color: #00aac5;
}
.skin-dark section[role="region"] > header:first-child a:active,
.skin-dark section[role="region"] > header:first-child button:active,
.skin-dark section[role="region"] > header:first-child a:hover,
.skin-dark section[role="region"] > header:first-child button:hover,
section[role="region"].skin-dark > header:first-child a:active,
section[role="region"].skin-dark > header:first-child button:active,
section[role="region"].skin-dark > header:first-child a:hover,
section[role="region"].skin-dark > header:first-child button:hover {
background-color: rgba(0,0,0,0.4);
}
.skin-dark section[role="region"] > header:first-child a:focus,
.skin-dark section[role="region"] > header:first-child button:focus,
section[role="region"].skin-dark > header:first-child a:focus,
section[role="region"].skin-dark > header:first-child button:focus {
background-color: transparent;
}
section[role="region"].skin-dark header h2,
.skin-dark > section[role="region"] header h2 {
background-color: #2c353b;
color: #fff;
border: none;
}
/* -----------------------------------------------------------------
HEADER SKIN: organic
.skin-organic will override default values
----------------------------------------------------------------- */
section[role="region"].skin-organic > header:first-child,
.skin-organic section[role="region"] > header:first-child {
color: #868692;
background-color: #f4f4f4;
}
section[role="region"].skin-organic > header:first-child h1,
.skin-organic section[role="region"] > header:first-child h1 {
color: #868692;
}
section[role="region"].skin-organic > header:first-child a,
section[role="region"].skin-organic > header:first-child button,
.skin-organic section[role="region"] > header:first-child a,
.skin-organic section[role="region"] > header:first-child button {
color: #00aac5;
}
section[role="region"].skin-organic > header:first-child a:focus,
section[role="region"].skin-organic > header:first-child button:focus,
.skin-organic section[role="region"] > header:first-child a:focus,
.skin-organic section[role="region"] > header:first-child button:focus {
background-color: transparent;
}
/* ----------------------------------
Icons
---------------------------------- */
html[dir="ltr"] section[role="region"].skin-organic > header:first-child .icon-back,
html[dir="ltr"] .skin-organic section[role="region"] > header:first-child .icon-back {
background-image: url(headers/images/icons/organic/left.png);
}
html[dir="rtl"] section[role="region"].skin-organic > header:first-child .icon-back,
html[dir="rtl"] .skin-organic section[role="region"] > header:first-child .icon-back {
background-image: url(headers/images/icons/organic/right.png);
}
section[role="region"].skin-organic > header:first-child .icon-close,
.skin-organic section[role="region"] > header:first-child .icon-close {
background-image: url(headers/images/icons/organic/close.png);
}
section[role="region"].skin-organic > header:first-child .icon-add,
.skin-organic section[role="region"] > header:first-child .icon-add {
background-image: url(headers/images/icons/organic/add.png);
}
html[dir="ltr"] section[role="region"].skin-organic > header:first-child .icon-edit,
html[dir="ltr"] .skin-organic section[role="region"] > header:first-child .icon-edit {
background-image: url(headers/images/icons/organic/edit.png);
}
html[dir="rtl"] section[role="region"].skin-organic > header:first-child .icon-edit,
html[dir="rtl"] .skin-organic section[role="region"] > header:first-child .icon-edit {
background-image: url(headers/images/icons/organic/edit-rtl.png);
}
/* ----------------------------------
Subheader
---------------------------------- */
section[role="region"].skin-organic header h2,
.skin-organic section[role="region"] header h2 {
background-color: #e7e7e7;
color: #4d4d4d;
border: none;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

View file

@ -0,0 +1,180 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="utf-8">
<title>Headers</title>
<meta name="description" content="Providing top-level navigation and inputs for the active view">
<!--
- This <style> and <link> is only used for the example preview,
- it isn't needed for the real use case.
-->
<link rel="stylesheet" href="../headers.css">
<style type="text/css">
html, body {
margin: 0;
padding: 0;
font-size: 10px;
background-color: #fff;
}
body {
background: none;
}
h2.bb-docs {
font-size: 1.8rem;
font-weight: 300;
color: #666;
margin: -0.1rem 0 0;
background-color: #f5f5f5;
padding: 0.4rem;
padding-inline-start: 3rem;
border: solid 0.1rem #e8e8e8;
margin: 3rem 0 1rem;
}
section[role="region"] {
margin-bottom: 1.5rem;
position: relative;
}
</style>
<script>
window.onload = function() {
document.getElementById('dir').checked = false;
};
</script>
</head>
<body role="application">
<p dir="ltr">
<label>
<input type="checkbox" id="dir"
onchange="document.documentElement.dir = this.checked ? 'rtl' : 'ltr'">
RTL mode
</label>
</p>
<section role="region">
<header>
<a href="#"><span class="icon icon-close">close</span></a>
<menu type="toolbar">
<button>done</button>
</menu>
<h1>Email</h1>
</header>
</section>
<section role="region">
<header>
<a href="#"></a>
<menu type="toolbar">
<button><span class="icon icon-send">send</span></button>
</menu>
<h1>Send message to X</h1>
</header>
</section>
<section role="region">
<header>
<a href="#"><span class="icon icon-back">back</span></a>
<menu type="toolbar">
<button><span class="icon icon-edit">edit</span></button>
<button><span class="icon icon-add">add</span></button>
</menu>
<h1>Email</h1>
</header>
<header>
<h2>Subheader text</h2>
</header>
</section>
<section role="region">
<header>
<button><span class="icon icon-menu">menu</span></button>
<menu type="toolbar">
<button><span class="icon icon-edit">edit</span></button>
<button><span class="icon icon-add">add</span></button>
</menu>
<h1>Inbox <em>(2)</em></h1>
</header>
</section>
<h2 class="bb-docs">Skin: Dark</h2>
<section role="region" class="skin-dark">
<header>
<a href="#"><span class="icon icon-close">close</span></a>
<menu type="toolbar">
<button>done</button>
</menu>
<h1>Music</h1>
</header>
</section>
<section role="region" class="skin-dark">
<header>
<a href="#"><span class="icon icon-back">back</span></a>
<menu type="toolbar">
<button><span class="icon icon-edit">edit</span></button>
<button><span class="icon icon-add">add</span></button>
</menu>
<h1>Music</h1>
</header>
<header>
<h2>Subheader text</h2>
</header>
</section>
<h2 class="bb-docs">Skin: Organic</h2>
<section role="region" class="skin-organic">
<header>
<a href="#"><span class="icon icon-close">close</span></a>
<menu type="toolbar">
<button>done</button>
</menu>
<h1>Settings</h1>
</header>
</section>
<div class="skin-organic">
<section role="region">
<header>
<a href="#"><span class="icon icon-back">back</span></a>
<menu type="toolbar">
<button><span class="icon icon-edit">edit</span></button>
<button><span class="icon icon-add">add</span></button>
</menu>
<h1>Settings</h1>
</header>
<header>
<h2>Subheader text</h2>
</header>
</section>
</div>
<h2 class="bb-docs">Skin: Comms</h2>
<div class="skin-comms">
<section role="region">
<header>
<a href="#"><span class="icon icon-close">close</span></a>
<menu type="toolbar">
<button>done</button>
</menu>
<h1>Contacts</h1>
</header>
</section>
<section role="region">
<header>
<a href="#"><span class="icon icon-back">back</span></a>
<menu type="toolbar">
<button><span class="icon icon-edit">edit</span></button>
<button><span class="icon icon-add">add</span></button>
</menu>
<h1>Contacts</h1>
</header>
<header>
<h2>Subheader text</h2>
</header>
</section>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,575 @@
/* ----------------------------------
* Input areas
* ---------------------------------- */
/**
* BiDi notes:
*
* - as the <input>, <textarea> and [contentEditable] elements are filled by
* the user, they need a `unicode-bidi: -moz-plaintext` attribute for their
* content to be displayed properly (e.g. LTR content in an RTL document,
* or vice-versa);
*
* - consequence: *-inline-start/end properties can still be used on such
* elements content direction doesn't impact element's direction
*
* - however, the one-line inputs must still be aligned along with the
* *document* direction: to the left in LTR, to the right in RTL
* -- no matter what the *content* direction is.
*/
html[dir="ltr"] input { text-align: left; }
html[dir="rtl"] input { text-align: right; }
input[type="text"],
input[type="password"],
input[type="email"],
input[type="tel"],
input[type="search"],
input[type="url"],
input[type="number"],
textarea {
font-family: sans-serif;
box-sizing: border-box;
display: block;
width: 100%;
height: 4rem;
line-height: 4rem;
resize: none;
padding: 0 1.5rem;
font-size: 1.6rem;
border: 0.1rem solid #c7c7c7;
border-radius: 0;
box-shadow: none; /* override the box-shadow from the system (performance issue) */
color: #333;
background: #fff;
margin: 0 0 1rem;
unicode-bidi: -moz-plaintext;
}
label:active {
background-color: transparent;
}
textarea {
height: 10rem;
max-height: 10rem;
line-height: 2rem;
}
input::-moz-placeholder,
textarea::-moz-placeholder {
color: #a9a9a9;
opacity: 1;
font-style: italic;
font-weight: 400;
}
input[disabled],
textarea[disabled] {
opacity: 0.5;
}
.skin-dark input[type="text"],
.skin-dark input[type="password"],
.skin-dark input[type="email"],
.skin-dark input[type="tel"],
.skin-dark input[type="search"],
.skin-dark input[type="url"],
.skin-dark input[type="number"],
.skin-dark textarea {
color: #fff;
background-color: transparent;
border-color: #454545;
}
form p {
position: relative;
margin: 0;
}
form p input + button[type="reset"],
form p textarea + button[type="reset"] {
position: absolute;
top: 0;
width: 4rem;
height: 4rem;
padding: 0;
border: none;
font-size: 0;
opacity: 0;
pointer-events: none;
background: url(input_areas/images/clear.png) no-repeat 50% 50% / 2.4rem auto;
}
html[dir="ltr"] form p input + button[type="reset"],
html[dir="ltr"] form p textarea + button[type="reset"] {
right: -0.3rem;
}
html[dir="rtl"] form p input + button[type="reset"],
html[dir="rtl"] form p textarea + button[type="reset"] {
left: -0.3rem;
}
.skin-dark p input + button[type="reset"],
.skin-dark p textarea + button[type="reset"] {
background-image: url(input_areas/images/clear_dark.png);
}
/* To avoid colission with BB butons */
li input + button[type="reset"]:after,
li textarea + button[type="reset"]:after {
background: none;
}
textarea {
padding: 1.2rem;
}
html[dir="ltr"] form p input:focus { padding-right: 3rem; }
html[dir="rtl"] form p input:focus { padding-left: 3rem; }
form p input:focus + button[type="reset"],
form p textarea:focus + button[type="reset"] {
opacity: 1;
pointer-events: all;
}
/* Fieldset */
fieldset {
position: relative;
overflow: hidden;
margin: 1.5rem 0 0;
padding: 0;
font-size: 2rem;
line-height: 1em;
background: none;
border: 0;
}
fieldset legend,
input[type="date"],
input[type="time"],
input[type="datetime"],
input[type="datetime-local"] {
border: 0.1rem solid #c7c7c7;
width: 100%;
box-sizing: border-box;
font-family: sans-serif;
font-style: italic;
font-size: 1.6rem;
margin: 0 0 1.5rem;
padding: 0 1.5rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
height: 4rem;
color: #333;
border-radius: 0;
background-color: #fff;
background-repeat: no-repeat;
}
html[dir="ltr"] fieldset legend,
html[dir="ltr"] input[type="date"],
html[dir="ltr"] input[type="time"],
html[dir="ltr"] input[type="datetime"],
html[dir="ltr"] input[type="datetime-local"] {
background-image: url(input_areas/images/dialog.svg);
background-position: right 1rem bottom 1rem;
}
html[dir="rtl"] fieldset legend,
html[dir="rtl"] input[type="date"],
html[dir="rtl"] input[type="time"],
html[dir="rtl"] input[type="datetime"],
html[dir="rtl"] input[type="datetime-local"] {
background-image: url(input_areas/images/dialog_rtl.svg);
background-position: left 1rem bottom 1rem;
}
fieldset legend {
margin: 0 0 1rem;
padding: 1rem 1.5rem 0;
background-color: rgba(0,0,0,0.05);
}
fieldset[disabled] {
opacity: 0.5;
pointer-events: none;
}
.skin-dark fieldset legend,
.skin-dark input[type="date"],
.skin-dark input[type="time"],
.skin-dark input[type="datetime"],
.skin-dark input[type="datetime-local"] {
color: #fff;
background-color: transparent;
border-color: #454545;
}
fieldset[disabled] legend,
input[type="date"][disabled],
input[type="time"][disabled],
input[type="datetime"][disabled],
input[type="datetime-local"][disabled] {
background-color: transparent;
}
html[dir="ltr"] fieldset[disabled] legend,
html[dir="ltr"] input[type="date"][disabled],
html[dir="ltr"] input[type="time"][disabled],
html[dir="ltr"] input[type="datetime"][disabled],
html[dir="ltr"] input[type="datetime-local"][disabled] {
background-image: url(input_areas/images/dialog_disabled.svg);
}
html[dir="rtl"] fieldset[disabled] legend,
html[dir="rtl"] input[type="date"][disabled],
html[dir="rtl"] input[type="time"][disabled],
html[dir="rtl"] input[type="datetime"][disabled],
html[dir="rtl"] input[type="datetime-local"][disabled] {
background-image: url(input_areas/images/dialog_disabled_rtl.svg);
}
input[type="date"]:focus,
input[type="time"]:focus,
input[type="datetime"]:focus,
input[type="datetime-local"]:focus {
box-shadow: none;
border-bottom-color: #c7c7c7;
}
.skin-dark input[type="date"]:focus,
.skin-dark input[type="time"]:focus,
.skin-dark input[type="datetime"]:focus,
.skin-dark input[type="datetime-local"]:focus {
box-shadow: none;
border-bottom-color: #454545;
}
input[type="date"]:active,
input[type="time"]:active,
input[type="datetime"]:active,
input[type="datetime-local"]:active {
background-color: #b2f2ff;
color: #fff;
}
.skin-dark input[type="date"]:active,
.skin-dark input[type="time"]:active,
.skin-dark input[type="datetime"]:active,
.skin-dark input[type="datetime-local"]:active {
background-color: #00aacc;
background-image: url(input_areas/images/dialog_active.svg);
}
/* fix for required inputs with wrong or empty value e.g. [type=email] */
input:invalid,
textarea:invalid,
.skin-dark input:invalid,
.skin-dark textarea:invalid {
color: #b90000;
}
input:focus,
textarea:focus,
.skin-dark input:focus,
.skin-dark textarea:focus {
box-shadow: inset 0 -0.1rem 0 #00caf2;
border-bottom-color: #00caf2;
}
input:invalid:focus,
textarea:invalid:focus,
.skin-dark input:invalid:focus,
.skin-dark textarea:invalid:focus {
box-shadow: inset 0 -0.1rem 0 #820000;
border-bottom-color: #820000;
}
input[type="range"]:focus,
.skin-dark input[type="range"]:focus {
box-shadow: none;
border-bottom: 0;
}
/* Tidy (search/submit) */
form[role="search"] {
position: relative;
height: 3.7rem;
background: #f4f4f4;
}
form[role="search"].skin-dark {
background: #202020;
}
form[role="search"] p {
padding: 0 1.5rem;
padding-inline-start: 3rem;
overflow: hidden;
position: relative;
}
form[role="search"] p input,
form[role="search"] p textarea {
height: 3.7rem;
border: none;
background: none;
padding: 0;
margin: 0;
box-shadow: none;
}
form[role="search"] p textarea {
padding: 1rem 0 0;
}
form[role="search"] p input::-moz-placeholder {
background-image: url(input_areas/images/search.svg);
background-repeat: no-repeat;
background-size: 3rem;
}
html[dir="ltr"] form[role="search"] p input::-moz-placeholder {
background-position: right -0.5rem center;
}
html[dir="rtl"] form[role="search"] p input::-moz-placeholder {
background-position: left -0.5rem center;
}
form[role="search"].skin-dark p input::-moz-placeholder {
background-image: url(input_areas/images/search_dark.svg);
}
form[role="search"] p input:focus::-moz-placeholder {
background: none;
}
form[role="search"] p input:invalid,
form[role="search"] p textarea:invalid,
form[role="search"] p input:focus,
form[role="search"] p textarea:focus {
border: none;
box-shadow: none;
}
form[role="search"].skin-dark p input,
form[role="search"].skin-dark p textarea {
color: #fff;
background: none;
box-shadow: none;
}
form[role="search"] button[type="submit"] {
min-width: 6rem;
height: 3.7rem;
padding: 0 1.5rem;
border: none;
color: #00aac5;
font-weight: normal;
font-size: 1.6rem;
line-height: 3.7rem;
width: auto;
border-radius: 0;
margin: 0;
position: relative;
background-image: none;
background-color: unset;
font-style: italic;
overflow: visible;
}
html[dir="ltr"] form[role="search"] button[type="submit"] { float: right; }
html[dir="rtl"] form[role="search"] button[type="submit"] { float: left; }
form[role="search"] button[type="submit"]:after {
content: "";
position: absolute;
offset-inline-start: -0.1rem;
top: 0.7rem;
bottom: 0.7rem;
width: 0.1rem;
background: #c7c7c7;
}
form[role="search"] button[type="submit"].icon {
font-size: 0;
background-repeat: no-repeat;
background-position: center;
background-size: 3rem;
}
form[role="search"] p input + button[type="reset"]{
height: 3.7rem;
offset-inline-end: 0.5rem;
}
form[role="search"].skin-dark button[type="submit"]:after {
background-color: #575757;
}
form[role="search"] button[type="submit"] + p > textarea {
height: 3.7rem;
line-height: 2rem;
}
form[role="search"] button[type="submit"] + p button {
height: 3.7rem;
}
form[role="search"].full button[type="submit"] {
display: none;
}
form[role="search"] button.icon:active,
form[role="search"] button[type="submit"]:active {
background-color: #b2f2ff;
color: #fff;
}
form[role="search"].skin-dark button.icon:active,
form[role="search"].skin-dark button[type="submit"]:active {
background: #00aacc;
}
form[role="search"] button[type="submit"][disabled] {
color: #adadad;
pointer-events: none;
}
form button::-moz-focus-inner {
border: none;
outline: none;
}
/* .bb-editable - e.g email & SMS recipients */
.bb-editable [contenteditable] {
display: inline-block;
box-sizing: border-box;
position: relative;
max-width: calc(100% - 1.4rem);
overflow: hidden;
padding: 0 1.5rem;
margin: 0 0 0.6rem;
line-height: 2.8rem;
border-radius: 1.4rem;
text-overflow: ellipsis;
font-size: 1.6rem;
font-style: italic;
white-space: nowrap;
color: #333;
background: #fff;
}
html[dir="ltr"] .bb-editable [contenteditable].icon {
padding-right: 3.7rem;
}
html[dir="rtl"] .bb-editable [contenteditable].icon {
padding-left: 3.7rem;
}
.bb-editable [contenteditable="false"].invalid {
background-color: #fedcdc;
padding: 0 1.5rem;
}
html[dir="ltr"] .bb-editable [contenteditable="false"].invalid { padding-left: 3.3rem; }
html[dir="rtl"] .bb-editable [contenteditable="false"].invalid { padding-right: 3.3rem; }
.bb-editable [contenteditable="false"].invalid:before {
content: "!";
position: absolute;
top: 0.6rem;
display: block;
width: 1.7rem;
height: 1.6rem;
padding: 0;
border-radius: 50%;
line-height: 1.6rem;
font-size: 1.2rem;
text-align: center;
text-indent: -0.1rem;
font-style: normal;
font-weight: 600;
color: #fff;
background-color: #b80404;
}
html[dir="ltr"] .bb-editable [contenteditable="false"].invalid:before { left: 0.8rem; }
html[dir="rtl"] .bb-editable [contenteditable="false"].invalid:before { right: 0.8rem; }
.bb-editable [contenteditable="false"].invalid:hover:before,
.bb-editable [contenteditable="false"].invalid:active:before {
color: #b2f2ff;
background-color: #fff;
}
.bb-editable [contenteditable]:hover,
.bb-editable [contenteditable]:active,
.bb-editable [contenteditable].invalid:hover,
.bb-editable [contenteditable].invalid:active {
color: #fff;
background-color: #b2f2ff;
}
.bb-editable [contenteditable].icon:after {
content: "";
position: absolute;
top: 0.7rem;
width: 3rem;
height: 3rem;
background-repeat: no-repeat;
background-size: 3rem;
}
html[dir="ltr"] .bb-editable [contenteditable].icon:after {
right: 0;
background-position: -0.7rem -3.8rem;
}
html[dir="rtl"] .bb-editable [contenteditable].icon:after {
left: 0;
background-position: 0.7rem -3.8rem;
}
html[dir="ltr"] .bb-editable [contenteditable].icon:hover:after,
html[dir="ltr"] .bb-editable [contenteditable].icon:active:after {
background-position: -0.7rem -0.8rem;
}
html[dir="rtl"] .bb-editable [contenteditable].icon:hover:after,
html[dir="rtl"] .bb-editable [contenteditable].icon:active:after {
background-position: 0.7rem -0.8rem;
}
html[dir="ltr"] .bb-editable [contenteditable].invalid.icon {
padding-right: 3.7rem;
}
html[dir="rtl"] .bb-editable [contenteditable].invalid.icon {
padding-left: 3.7rem;
}
html[dir="ltr"] .bb-editable [contenteditable].invalid.icon:after {
background-position: -0.7rem -6.8rem;
}
html[dir="rtl"] .bb-editable [contenteditable].invalid.icon:after {
background-position: 0.7rem -6.8rem;
}
html[dir="ltr"] .bb-editable [contenteditable].invalid.icon:hover:after,
html[dir="ltr"] .bb-editable [contenteditable].invalid.icon:active:after {
background-position: -0.7rem -0.8rem;
}
html[dir="rtl"] .bb-editable [contenteditable].invalid.icon:hover:after,
html[dir="rtl"] .bb-editable [contenteditable].invalid.icon:active:after {
background-position: 0.7rem -0.8rem;
}
.bb-editable [contenteditable="true"],
.bb-editable [contenteditable="true"]:hover,
.skin-comms .bb-editable [contenteditable="true"]:hover {
padding: 0 0.2rem;
white-space: normal;
font-size: 1.8rem;
font-style: normal;
color: #000;
background: none repeat scroll 0 0 transparent;
}
.skin-comms .bb-editable [contenteditable]:hover,
.skin-comms .bb-editable [contenteditable]:active {
color: #fff;
background-color: #29bbb5;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 9 9"><path fill="#00AAC5" enable-background="new" d="M9 0L9 9 0 9"/></svg>

After

Width:  |  Height:  |  Size: 148 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 9 9"><path fill="#B2F2FF" d="M9 0v9h-9"/></svg>

After

Width:  |  Height:  |  Size: 121 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 9 9"><path opacity=".5" fill="#A9A9A9" d="M9 0L9 9 0 9"/></svg>

After

Width:  |  Height:  |  Size: 137 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 9 9"><path opacity=".5" fill="#A9A9A9" enable-background="new" d="M0 0L0 9 9 9"/></svg>

After

Width:  |  Height:  |  Size: 161 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 9 9"><path fill="#00AAC5" enable-background="new" d="M0 0L0 9 9 9"/></svg>

After

Width:  |  Height:  |  Size: 148 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30"><path fill-rule="evenodd" clip-rule="evenodd" fill="#424242" d="M23.385 21.494c.266.266.057.903-.465 1.426s-1.16.729-1.426.465l-4.041-4.039c-1.01.656-2.21 1.047-3.505 1.047-3.561 0-6.448-2.885-6.448-6.446 0-3.56 2.887-6.446 6.448-6.446 3.562 0 6.449 2.887 6.449 6.446 0 1.297-.393 2.499-1.051 3.509l4.041 4.039h-.002zm-9.437-12.459c-2.712 0-4.914 2.198-4.914 4.912s2.202 4.913 4.914 4.913c2.714 0 4.914-2.199 4.914-4.913-.001-2.714-2.2-4.912-4.914-4.912z"/></svg>

After

Width:  |  Height:  |  Size: 546 B

Some files were not shown because too many files have changed in this diff Show more