Skip to content

Instantly share code, notes, and snippets.

@NicolasDurant
Last active August 23, 2023 19:57
Show Gist options
  • Save NicolasDurant/5894019ae8cbeb6633f5228d2749baa1 to your computer and use it in GitHub Desktop.
Save NicolasDurant/5894019ae8cbeb6633f5228d2749baa1 to your computer and use it in GitHub Desktop.
[Vue3 / HTML / JS / CSS - Tailwind theme components build in Vue3] This is a whole custom flat design implementation with colors and custom forms, built with tailwind css and Vue3. It contains the necessary tailwind config and css basis, and the implementation of 16! basic design elements like cards, buttons, lists, tables, etc. in Vue3. All com…
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
/* WRAPPER FOR INPUTS */
label.form-input-wrapper {
@apply flex flex-col justify-start relative
}
/* WRAPPER FOR ERRORS */
label.form-input-wrapper > .form-errors {
@apply px-4 pt-1 relative list-disc
}
/* ERROR MSG */
label.form-input-wrapper > .form-errors > .form-error-msg {
@apply text-error text-xs
}
/* LABEL OF INPUT - DEFAULT */
label.form-input-wrapper > span.form-label {
width: fit-content;
@apply text-primary
px-4 pt-1
rounded-t-md
border-2 border-b-0 border-r-4 border-shades
}
/* LABEL OF INPUT - INPUT FOCUSED */
label.form-input-wrapper:focus-within > span.form-label:not([readonly],[disabled]) {
@apply text-white bg-primary border-primary
}
/* LABEL OF INPUT - HOVER */
label.form-input-wrapper:hover > span.form-label:not([readonly],[disabled]) {
@apply text-white bg-primary border-primary bg-opacity-70 border-opacity-70
}
/* LABEL OF INPUT - HAS ERROR */
label.form-input-wrapper.form-has-errors > span.form-label {
@apply text-error
border-error
}
/* LABEL OF INPUT - HAS ERROR INPUT FOCUSED */
label.form-input-wrapper.form-has-errors:focus-within > span.form-label:not([readonly],[disabled]) {
@apply text-white bg-error border-error
}
/* LABEL OF INPUT - HOVER HAS ERROR */
label.form-input-wrapper.form-has-errors:hover > span.form-label:not([readonly],[disabled]) {
@apply text-white bg-error border-error bg-opacity-70 border-opacity-70
}
/* LABEL OF INPUT - READONLY */
label.form-input-wrapper > span[readonly].form-label {
@apply text-shades-accent-dark
border-shades-dark
}
/* LABEL OF INPUT - DISABLED */
label.form-input-wrapper > span[disabled].form-label {
@apply text-shades-accent-dark
border-shades-accent cursor-not-allowed
}
/* INPUT - DEFAULT */
label.form-input-wrapper > input[type='text'],
label.form-input-wrapper > input[type='email'],
label.form-input-wrapper > input[type='password'],
label.form-input-wrapper > input[type='number'],
label.form-input-wrapper > input[type='date'],
label.form-input-wrapper > input[type='datetime-local'],
label.form-input-wrapper > input[type='month'],
label.form-input-wrapper > input[type='week'],
label.form-input-wrapper > input[type='search'],
label.form-input-wrapper > input[type='tel'],
label.form-input-wrapper > input[type='time'],
label.form-input-wrapper > input[multiple],
label.form-input-wrapper > textarea,
label.form-input-wrapper > select
{
@apply mt-0 block
w-full px-4 pt-2 pr-8
rounded-md rounded-tl-none
border-2 border-r-8 border-b-8 border-shades
focus:ring-0 focus:border-primary
}
/* INPUT - DEFAULT, HOVER */
label.form-input-wrapper:hover > input[type='text'],
label.form-input-wrapper:hover > input[type='email'],
label.form-input-wrapper:hover > input[type='password'],
label.form-input-wrapper:hover > input[type='number'],
label.form-input-wrapper:hover > input[type='date'],
label.form-input-wrapper:hover > input[type='datetime-local'],
label.form-input-wrapper:hover > input[type='month'],
label.form-input-wrapper:hover > input[type='week'],
label.form-input-wrapper:hover > input[type='search'],
label.form-input-wrapper:hover > input[type='tel'],
label.form-input-wrapper:hover > input[type='time'],
label.form-input-wrapper:hover > input[multiple],
label.form-input-wrapper:hover > textarea,
label.form-input-wrapper:hover > select
{
@apply border-primary border-opacity-70
}
/* INPUT - HAS ERRORS */
label.form-input-wrapper.form-has-errors > input[type='text'],
label.form-input-wrapper.form-has-errors > input[type='email'],
label.form-input-wrapper.form-has-errors > input[type='password'],
label.form-input-wrapper.form-has-errors > input[type='number'],
label.form-input-wrapper.form-has-errors > input[type='date'],
label.form-input-wrapper.form-has-errors > input[type='datetime-local'],
label.form-input-wrapper.form-has-errors > input[type='month'],
label.form-input-wrapper.form-has-errors > input[type='week'],
label.form-input-wrapper.form-has-errors > input[type='search'],
label.form-input-wrapper.form-has-errors > input[type='tel'],
label.form-input-wrapper.form-has-errors > input[type='time'],
label.form-input-wrapper.form-has-errors > input[multiple],
label.form-input-wrapper.form-has-errors > textarea,
label.form-input-wrapper.form-has-errors > select
{
@apply border-error
}
/* INPUT - HAS ERRORS, HOVER */
label.form-input-wrapper.form-has-errors:hover > input[type='text'],
label.form-input-wrapper.form-has-errors:hover > input[type='email'],
label.form-input-wrapper.form-has-errors:hover > input[type='password'],
label.form-input-wrapper.form-has-errors:hover > input[type='number'],
label.form-input-wrapper.form-has-errors:hover > input[type='date'],
label.form-input-wrapper.form-has-errors:hover > input[type='datetime-local'],
label.form-input-wrapper.form-has-errors:hover > input[type='month'],
label.form-input-wrapper.form-has-errors:hover > input[type='week'],
label.form-input-wrapper.form-has-errors:hover > input[type='search'],
label.form-input-wrapper.form-has-errors:hover > input[type='tel'],
label.form-input-wrapper.form-has-errors:hover > input[type='time'],
label.form-input-wrapper.form-has-errors:hover > input[multiple],
label.form-input-wrapper.form-has-errors:hover > textarea,
label.form-input-wrapper.form-has-errors:hover > select
{
@apply border-error border-opacity-70
}
/* INPUT - READONLY */
label.form-input-wrapper > input[type='text'][readonly],
label.form-input-wrapper > input[type='email'][readonly],
label.form-input-wrapper > input[type='password'][readonly],
label.form-input-wrapper > input[type='number'][readonly],
label.form-input-wrapper > input[type='date'][readonly],
label.form-input-wrapper > input[type='datetime-local'][readonly],
label.form-input-wrapper > input[type='month'][readonly],
label.form-input-wrapper > input[type='week'][readonly],
label.form-input-wrapper > input[type='search'][readonly],
label.form-input-wrapper > input[type='tel'][readonly],
label.form-input-wrapper > input[type='time'][readonly],
label.form-input-wrapper > input[multiple][readonly],
label.form-input-wrapper > textarea[readonly],
label.form-input-wrapper > select[readonly]
{
@apply text-shades-accent-dark
border-shades-dark
}
/* INPUT - DISABLED */
label.form-input-wrapper > input[type='text'][disabled],
label.form-input-wrapper > input[type='email'][disabled],
label.form-input-wrapper > input[type='password'][disabled],
label.form-input-wrapper > input[type='number'][disabled],
label.form-input-wrapper > input[type='date'][disabled],
label.form-input-wrapper > input[type='datetime-local'][disabled],
label.form-input-wrapper > input[type='month'][disabled],
label.form-input-wrapper > input[type='week'][disabled],
label.form-input-wrapper > input[type='search'][disabled],
label.form-input-wrapper > input[type='tel'][disabled],
label.form-input-wrapper > input[type='time'][disabled],
label.form-input-wrapper > input[multiple][disabled],
label.form-input-wrapper > textarea[disabled],
label.form-input-wrapper > select[disabled]
{
@apply text-shades-accent-dark
border-shades-accent cursor-not-allowed
}
/* INPUT PLACEHOLDER - DEFAULT */
label.form-input-wrapper > input[type='text']::placeholder,
label.form-input-wrapper > input[type='email']::placeholder,
label.form-input-wrapper > input[type='password']::placeholder,
label.form-input-wrapper > input[type='number']::placeholder,
label.form-input-wrapper > input[type='date']::placeholder,
label.form-input-wrapper > input[type='datetime-local']::placeholder,
label.form-input-wrapper > input[type='month']::placeholder,
label.form-input-wrapper > input[type='week']::placeholder,
label.form-input-wrapper > input[type='search']::placeholder,
label.form-input-wrapper > input[type='tel']::placeholder,
label.form-input-wrapper > input[type='time']::placeholder,
label.form-input-wrapper > input[multiple]::placeholder,
label.form-input-wrapper > textarea::placeholder,
label.form-input-wrapper > select::placeholder
{
@apply text-shades-dark
}
/* INPUT PLACEHOLDER - READONLY */
label.form-input-wrapper > input[type='text'][readonly]::placeholder,
label.form-input-wrapper > input[type='email'][readonly]::placeholder,
label.form-input-wrapper > input[type='password'][readonly]::placeholder,
label.form-input-wrapper > input[type='number'][readonly]::placeholder,
label.form-input-wrapper > input[type='date'][readonly]::placeholder,
label.form-input-wrapper > input[type='datetime-local'][readonly]::placeholder,
label.form-input-wrapper > input[type='month'][readonly]::placeholder,
label.form-input-wrapper > input[type='week'][readonly]::placeholder,
label.form-input-wrapper > input[type='search'][readonly]::placeholder,
label.form-input-wrapper > input[type='tel'][readonly]::placeholder,
label.form-input-wrapper > input[type='time'][readonly]::placeholder,
label.form-input-wrapper > input[multiple][readonly]::placeholder,
label.form-input-wrapper > textarea[readonly]::placeholder,
label.form-input-wrapper > select[readonly]::placeholder
{
@apply text-white
}
/* INPUT PLACEHOLDER - DISABLED */
label.form-input-wrapper > input[type='text'][disabled]::placeholder,
label.form-input-wrapper > input[type='email'][disabled]::placeholder,
label.form-input-wrapper > input[type='password'][disabled]::placeholder,
label.form-input-wrapper > input[type='number'][disabled]::placeholder,
label.form-input-wrapper > input[type='date'][disabled]::placeholder,
label.form-input-wrapper > input[type='datetime-local'][disabled]::placeholder,
label.form-input-wrapper > input[type='month'][disabled]::placeholder,
label.form-input-wrapper > input[type='week'][disabled]::placeholder,
label.form-input-wrapper > input[type='search'][disabled]::placeholder,
label.form-input-wrapper > input[type='tel'][disabled]::placeholder,
label.form-input-wrapper > input[type='time'][disabled]::placeholder,
label.form-input-wrapper > input[multiple][disabled]::placeholder,
label.form-input-wrapper > textarea[disabled]::placeholder,
label.form-input-wrapper > select[disabled]::placeholder
{
@apply text-shades-accent-dark
}
/* CURSOR FOR "MENUS" */
label.form-input-wrapper > select,
label.form-input-wrapper:hover > input[type='date'],
label.form-input-wrapper:hover > input[type='datetime-local'],
label.form-input-wrapper:hover > input[type='month'],
label.form-input-wrapper:hover > input[type='week'],
label.form-input-wrapper:hover > input[type='time']
{
@apply cursor-pointer
}
/* SELECT OVERRIDE */
label.form-input-wrapper > select:not([multiple]){
@apply pr-8
}
/* SELECT MULTIPLE OVERRIDE, SADGE DOESNT WORK */
label.form-input-wrapper > select[multiple] > option:checked{
@apply text-white bg-primary
}
/* WRAPPER FOR CHECKBOXES, RADIO */
label.form-box-wrapper {
@apply flex items-center justify-start gap-x-2 cursor-pointer
}
/* CHECKBOXES, RADIO - DEFAULT */
label.form-box-wrapper > input[type='checkbox'],
label.form-box-wrapper > input[type='radio']{
@apply border-shades-accent-dark
focus:ring-primary-dark
}
/* CHECKBOXES, RADIO - HOVER */
label.form-box-wrapper:hover > input[type='checkbox'],
label.form-box-wrapper:hover > input[type='radio']{
@apply border-opacity-70 bg-primary bg-opacity-70
}
/* CHECKBOXES, RADIO - CHECKED */
label.form-box-wrapper > input[type='checkbox']:checked,
label.form-box-wrapper > input[type='radio']:checked{
@apply bg-primary border-primary-dark
focus:ring-primary-dark
}
}
<template>
<div v-if="showMe" :class="[`variant-${variant}`]" class="rounded-md p-4">
<div class="flex">
<!-- LEADING ICON -->
<div class="flex-shrink-0 icon">
<CheckCircleIcon v-if="variant === 'success'" class="h-5 w-5" aria-hidden="true"/>
<InformationCircleIcon v-if="variant === 'info'" class="h-5 w-5 text-blue-400" aria-hidden="true"/>
<ExclamationTriangleIcon v-if="variant === 'warn'" class="h-5 w-5" aria-hidden="true"/>
<XCircleIcon v-if="variant === 'error'" class="h-5 w-5" aria-hidden="true"/>
</div>
<!-- LABEL -->
<div class="ml-3">
<p class="text-sm font-medium">{{ label }}</p>
<slot></slot>
</div>
<!-- CLOSE -->
<div class="ml-auto pl-3">
<div class="-mx-1.5 -my-1.5">
<button type="button"
@click="closeMe"
class="close-x inline-flex rounded-md p-1.5">
<XMarkIcon class="h-5 w-5" aria-hidden="true"/>
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import {
CheckCircleIcon,
XMarkIcon,
ExclamationTriangleIcon,
XCircleIcon,
InformationCircleIcon
} from '@heroicons/vue/20/solid'
export default {
components: {
CheckCircleIcon, XMarkIcon, ExclamationTriangleIcon, XCircleIcon, InformationCircleIcon
},
data() {
return {showMe: true}
},
props: {
label: {
type: String,
default: null,
required: false,
},
variant: {
type: String,
default: 'success',
required: false,
validator: (value) => {
return ['success', 'info', 'warn', 'error'].includes(value)
},
},
},
methods: {
closeMe() {
this.$emit('alert-closed')
this.showMe = false
}
}
}
</script>
<style scoped>
.variant-success {
@apply bg-success bg-opacity-25 text-success-dark;
}
.variant-success .icon {
@apply text-success;
}
.variant-success .close-x {
@apply bg-success bg-opacity-0 hover:bg-opacity-30 focus:outline-none focus:ring-2 focus:ring-success-dark;
}
.variant-info {
@apply bg-secondary bg-opacity-25 text-secondary-dark;
}
.variant-info .icon {
@apply text-secondary;
}
.variant-info .close-x {
@apply bg-secondary bg-opacity-0 hover:bg-opacity-30 focus:outline-none focus:ring-2 focus:ring-secondary-dark;
}
.variant-warn {
@apply bg-warn bg-opacity-25 text-warn-dark;
}
.variant-warn .icon {
@apply text-warn;
}
.variant-warn .close-x {
@apply bg-warn bg-opacity-0 hover:bg-opacity-30 focus:outline-none focus:ring-2 focus:ring-warn-dark;
}
.variant-error {
@apply bg-error bg-opacity-25 text-error-dark;
}
.variant-error .icon {
@apply text-error;
}
.variant-error .close-x {
@apply bg-error bg-opacity-0 hover:bg-opacity-30 focus:outline-none focus:ring-2 focus:ring-error-dark;
}
</style>
<template>
<button :class="[`size-${size}`, `bg-${color}-100`, `text-${color}-800`, `focus:border-${color}-800`, ]" class="inline-flex items-center rounded-md font-medium cursor-pointer hover:bg-opacity-90 border-2 border-opacity-0 focus:border-opacity-100">
{{ label }}</button>
</template>
<script>
export default {
props: {
color: {
type: String,
default: '',
required: false,
},
label: {
type: String,
default: 'Badge',
required: false,
},
size: {
type: String,
default: 'base',
required: false,
validator: (value) => {
return ['sm', 'base', 'md', 'lg'].includes(value)
},
},
}
}
</script>
<style scoped>
.size-sm {
@apply px-2 py-0.5 text-xs;
}
.size-base {
@apply px-2.5 py-0.5 text-sm shadow-sm;
}
.size-md {
@apply px-3.5 py-1.5 text-base shadow-md;
}
.size-lg {
@apply px-5 py-3 text-lg shadow-lg;
}
</style>
<template>
<button :disabled="disabled" type="button"
:class="[`size-${size}`, `variant-${variant}`]"
class="inline-flex items-center rounded-md border-2 gap-x-2 py-2.5 px-3.5 font-semibold text-white relative focus:ring-2 whitespace-nowrap">
<!-- LEADING LOADING INDICATOR -->
<svg :class="[`icon-size-${size}`]" v-if="loading && icon === 'leading'" xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="animate-spin">
<path stroke-linecap="round" stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"/>
</svg>
<!-- LEADING ICON -->
<span v-if="!loading && icon === 'leading'" :class="[`icon-size-${size}`, !label || '-ml-0.5']">
<slot name="leading"></slot>
</span>
<!-- LABEL -->
{{ label }}
<!-- TRAILING ICON -->
<span v-if="!loading && icon === 'trailing'" :class="[`icon-size-${size}`, !label || '-mr-0.5']">
<slot name="trailing"></slot>
</span>
<!-- TRAILING LOADING INDICATOR -->
<svg :class="[`icon-size-${size}`]" v-if="loading && icon === 'trailing'" xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="animate-spin">
<path stroke-linecap="round" stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"/>
</svg>
</button>
</template>
<script>
export default {
props: {
disabled: {
type: Boolean,
default: false,
required: false,
},
icon: {
type: String,
default: '',
required: false,
validator: (value) => {
return ['', 'leading', 'trailing'].includes(value)
},
},
loading: {
type: Boolean,
default: false,
required: false,
},
label: {
type: String,
default: null,
required: false,
},
size: {
type: String,
default: 'base',
required: false,
validator: (value) => {
return ['sm', 'base', 'md', 'lg'].includes(value)
},
},
variant: {
type: String,
default: 'primary',
required: false,
validator: (value) => {
return ['primary', 'secondary', 'tertiary', 'outline', 'warn', 'error'].includes(value)
},
},
},
}
</script>
<style scoped>
button:disabled {
@apply bg-shades-accent border-shades-accent-dark cursor-not-allowed;
}
button:disabled .variant-outline {
@apply bg-shades-dark;
}
.icon-size-sm {
@apply w-4 h-4;
}
.icon-size-base {
@apply w-5 h-5;
}
.icon-size-md {
@apply w-6 h-6;
}
.icon-size-lg {
@apply w-7 h-7;
}
.size-sm {
@apply text-xs;
}
.size-base {
@apply text-sm shadow-sm;
}
.size-md {
@apply text-base shadow-md;
}
.size-lg {
@apply text-lg shadow-lg;
}
.variant-primary {
@apply bg-primary border-primary-dark hover:bg-opacity-90 focus:ring-primary-dark;
}
.variant-secondary {
@apply bg-secondary border-secondary-dark hover:bg-opacity-90 focus:ring-secondary-dark;
}
.variant-tertiary {
@apply bg-tertiary border-tertiary-dark hover:bg-opacity-90 focus:ring-tertiary-dark;
}
.variant-outline {
@apply bg-white text-black border-black hover:bg-opacity-90 focus:ring-black;
}
.variant-warn {
@apply bg-warn border-warn-dark hover:bg-opacity-90 focus:ring-warn-dark;
}
.variant-error {
@apply bg-error border-error-dark hover:bg-opacity-90 focus:ring-error-dark;
}
</style>
<template>
<div
class="bg-white rounded-xl shadow-2xl w-full px-4 py-4 md:px-5 md:py-8 2xl:px-8 2xl:py-10 relative overflow-auto"
style="max-height: 82vh">
<slot></slot>
</div>
</template>
<script>
export default {}
</script>
<template>
<div
class="flex flex-col justify-center items-end"
>
<!-- DRAG & DROP AREA -->
<div
class="w-full h-full bg-shades border-2 border-dotted rounded-md py-8 px-2 flex flex-col justify-center items-center hover:bg-opacity-70 hover:text-opacity-70"
:class="[classes, wrongCount || wrongSize || wrongFile ? 'border-warn-dark' : 'border-primary-dark']"
@dragover.prevent="dragOver"
@dragleave.prevent="dragLeave"
@drop.prevent="drop($event)"
>
<!-- FILE PREVIEW -->
<div v-if="uploadedFile" class="flex flex-col justify-center items-center">
<DocumentArrowUpIcon class="h-12 w-12 text-primary"></DocumentArrowUpIcon>
<h2 class="text-2xl text-primary mt-4">{{fileName}}</h2>
<p>
Die Datei ist bereit zum Hochladen
</p>
</div>
<!-- INFO TEXT -->
<div
v-if="
!uploadedFile &&
!wrongFile &&
!wrongCount &&
!wrongSize
"
class="flex flex-col justify-center items-center"
>
<ArrowDownTrayIcon class="h-12 w-12 text-primary"></ArrowDownTrayIcon>
<h2 class="text-2xl text-primary mt-4">.XLIFF</h2>
<p>
Eine Übersetzungs-Datei in diesem Feld ablegen
</p>
</div>
<!-- WRONG FILE TYPE -->
<div
v-if="wrongFile"
class="info-text flex flex-col justify-center items-center"
>
<DocumentMinusIcon class="h-12 w-12 text-warn"></DocumentMinusIcon>
<h2 class="text-2xl text-warn mt-4">Falscher Datei-Typ</h2>
<p>
Es muss eine Datei von Typ ".xliff" hochgeladen werden
</p>
</div>
<!-- WRONG FILE COUNT (x>1) -->
<div
v-if="wrongCount"
class="info-text flex flex-col justify-center items-center"
>
<DocumentMinusIcon class="h-12 w-12 text-warn"></DocumentMinusIcon>
<h2 class="text-2xl text-warn mt-4">Zu viele Dateien</h2>
<p>
Bitte maximal eine Datei hochladen
</p>
</div>
<!-- WRONG FILE SIZE (x>10MB) -->
<div
v-if="wrongSize"
class="info-text flex flex-col justify-center items-center"
>
<DocumentMinusIcon class="h-12 w-12 text-warn"></DocumentMinusIcon>
<h2 class="text-2xl text-warn mt-4">Datei ist zu groß</h2>
<p>
Die maximale Upload-Größe beträgt 10MB
</p>
</div>
</div>
<!-- FILE INPUT (HIDDEN) -->
<input ref="myFile" type="file" accept="application/x-xliff+xml" @change="previewFile"/>
<!-- BUTTON (TRIGGERS INPUT) -->
<div class="flex justify-center items-center gap-x-2 mt-4">
<span>Alternativ:</span>
<BaseButton @click="$refs.myFile.click()" label="Datei auswählen" variant="outline"></BaseButton>
<BaseButton @click="$emit('upload', uploadedFile)" :disabled="uploadedFile === ''" label="Hochladen"></BaseButton>
</div>
</div>
</template>
<script>/**
* @author Nicolas Durant
* @description Component that provides an area for the user to drop an image onto. It's restricted to image file types only.
* As soon as the image is uploaded it will trigger an emit event 'uploaded', which the parent can listen to. Also includes the ChooseImage Component,
* but without adding another image preview.
* @event @uploaded
*/
import {ArrowDownTrayIcon, DocumentArrowUpIcon, DocumentMinusIcon} from "@heroicons/vue/24/outline";
import BaseButton from "./BaseButton";
export default {
components: {
ArrowDownTrayIcon,
DocumentMinusIcon,
DocumentArrowUpIcon,
BaseButton
},
data() {
return {
/**
* Control for displaying user guidance.
* @type {boolean}
*/
isDragging: false,
/**
* Control for displaying user guidance. File type !== image.
* @type {boolean}
*/
wrongFile: false,
/**
* Control for displaying user guidance. Count > 1 error.
* @type {boolean}
*/
wrongCount: false,
/**
* Control for displaying user guidance. Size > 10MB error.
* @type {boolean}
*/
wrongSize: false,
/**
* The original file name..
* @type {string}
*/
fileName: '',
/**
* Holds the image uploaded by the user.
* @type {string}
*/
uploadedFile: ''
}
},
methods: {
/**
* Gets called when the user drags a file above the area.
*/
dragOver() {
this.isDragging = true
},
/**
* Gets called when the user drags a file out of the area.
*/
dragLeave() {
this.isDragging = false
},
/**
* Validates the file & generates a file reader, when the user drops an image into the area.
* @param e {event} - event that gets provided by the drop API
*/
drop(e) {
const files = e.dataTransfer.files
if (this.filesValid(files)) {
this.fileReader(files[0])
}
},
/**
* Generates a file reader for a given file.
* Will then emit the uploaded event when finished. Also checks for the correct file type and shows a notification otherwise.
* @param file - file provided by drag & drop, or selection
*/
fileReader(file) {
const reader = new FileReader()
reader.onload = (e) => {
this.wrongCount = false
this.wrongFile = false
this.wrongSize = false
this.uploadedFile = e.target.result
this.isDragging = false
}
reader.readAsDataURL(file)
},
/**
* Validates the given files.
* 1. there should only be 1 file
* 2. it has to be an xliff file
* 3. it has to be smaller than 10MB
* @param files - files provided by drag & drop, or selection
* @return {boolean} - true when all tests passed
*/
filesValid(files) {
this.wrongCount = false
this.wrongFile = false
this.wrongSize = false
this.fileName = ''
// only 1 file
if (files.length > 1) {
this.wrongCount = true
this.uploadedFile = ''
this.isDragging = false
return false
}
const file = files[0]
this.fileName = file.name
// only xliff files
if (!file.type.includes("application/x-xliff+xml")) {
this.uploadedFile = ''
this.wrongFile = true
this.isDragging = false
return false
}
// only <10MB
if (file.size / 1000000 > 10) {
this.uploadedFile = ''
this.wrongSize = true
this.isDragging = false
return false
}
return true
},
/**
* Validates the file & generates a file reader, so the user can select an image from their filesystem to use.
*/
previewFile() {
const files = this.$refs.myFile.files
if (this.filesValid(files)) {
this.fileReader(files[0])
}
}
},
computed: {
/**
* Computed property for dynamic classes, depending on the state of the component.
*/
classes() {
return {isDragging: this.isDragging}
}
}
}
</script>
<style scoped>
.isDragging {
@apply bg-opacity-70 text-opacity-70;
}
img {
width: 100%;
height: 100%;
object-fit: contain;
}
input[type='file'] {
display: none;
}
</style>
<template>
<div class="inline-block relative">
<div @click="showMe = !showMe" class="cursor-pointer">
<BaseButton v-if="!hasDefaultSlot" icon="trailing" :label="label">
<template v-slot:trailing>
<ChevronUpIcon v-if="showMe"/>
<ChevronDownIcon v-else/>
</template>
</BaseButton>
<slot></slot>
</div>
<ul :class="openTo === 'right' ? '' : 'right-0'" v-show="showMe"
class="absolute z-30 block rounded-md border-2 border-shades shadow-xl mt-1 cursor-pointer">
<BaseDropDownNode v-for="(level, index) in levels" :first="index === 0" :last="index === levels.length-1"
:node="level" :open-to="openTo"></BaseDropDownNode>
</ul>
</div>
</template>
<script>
import BaseDropDownNode from "./BaseDropDownNode";
import BaseButton from "./BaseButton";
import {ChevronDownIcon, ChevronUpIcon} from "@heroicons/vue/24/solid";
export default {
components: {BaseButton, BaseDropDownNode, ChevronDownIcon, ChevronUpIcon},
props: {
label: {
type: String,
default: 'Label'
},
levels: {
type: Array,
},
openTo: {
type: String,
default: 'right',
validator: (value) => {
return ['left', 'right'].includes(value)
},
},
},
data() {
return {
showMe: false
}
},
methods: {
hideMe() {
this.showMe = false
}
},
computed: {
hasDefaultSlot() {
return !!this.$slots.default;
}
}
}
</script>
<template>
<li @click="node.method"
:class="[
!node.children || 'dropdown',
!node.divider || 'border-b border-shades',
!first || 'rounded-t-md',
!last || 'rounded-b-md',
]"
class="bg-white text-black hover:bg-primary hover:text-shades py-2 px-4 block ">
<button :class="openTo === 'right' ? 'justify-start' : 'justify-end'"
class="whitespace-nowrap flex items-center w-full">
<Component class="h-5 w-5 mr-2" v-if="node.icon && openTo === 'right'" :is="node.icon"/>
<ChevronLeftIcon class="h-4 w-4 mr-auto" v-if="node.children && openTo === 'left'"></ChevronLeftIcon>
<span :class="openTo === 'right' ? 'mr-2' : 'ml-2'">{{ node.label }}</span>
<Component class="h-5 w-5 ml-2" v-if="node.icon && openTo === 'left'" :is="node.icon"/>
<ChevronRightIcon class="h-4 w-4 ml-auto" v-if="node.children && openTo === 'right'"></ChevronRightIcon>
</button>
<div :class="openTo === 'right' ? 'pl-1 left-full' : 'pr-1 right-full'"
class="dropdown-content absolute z-30 hidden -mt-8" v-if="node.children && node.children.length">
<ul class="shadow-xl rounded-md border-2 border-shades">
<BaseDropDownNode :open-to="openTo" v-for="(child, index) in node.children" :first="index === 0" :last="index === node.children.length-1" :node="child"></BaseDropDownNode>
</ul>
</div>
</li>
</template>
<script>
import {BookOpenIcon, ChevronLeftIcon, ChevronRightIcon} from "@heroicons/vue/24/solid";
export default {
components: {
BookOpenIcon,
ChevronLeftIcon,
ChevronRightIcon,
},
name: "BaseDropDownNode",
props: ['first', 'last', 'node', 'openTo']
};
</script>
<style scoped>
.dropdown:hover > .dropdown-content {
display: block;
}
</style>
<template>
<div
@click="copyToClipboard"
class="absolute right-2 top-9 rounded-full h-8 w-8 flex justify-center items-center group hover:bg-primary hover:bg-opacity-50 cursor-pointer"
>
<ClipboardDocumentCheckIcon
class="h-6 w-6 text-primary-dark group-hover:text-white"></ClipboardDocumentCheckIcon>
</div>
</template>
<script>
import {ClipboardDocumentCheckIcon} from "@heroicons/vue/24/outline";
import {mapActions} from "pinia";
import {useNotificationStore} from "../../stores/notification";
export default {
props: ['copy'],
components: {
ClipboardDocumentCheckIcon
},
methods: {
copyToClipboard() {
navigator.clipboard.writeText(this.copy)
this.addNotification({variant: 'info', label: 'Wert kopiert!', text: 'Wert wurde in die Zwischenablage gelegt.'})
},
...mapActions(useNotificationStore, ['addNotification']),
}
}
</script>
<style scoped>
</style>
<template>
<div class="overflow-hidden bg-white border border-shades rounded-md w-full">
<ul role="list" class="divide-y divide-shades">
<slot></slot>
</ul>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
<template>
<li>
<div @click="$emit('item-clicked', item)"
:class="[
disabled ? 'bg-shades-dark border-shades-accent-dark cursor-not-allowed text-white hover:bg-opacity-90' : 'hover:bg-primary hover:text-white hover:bg-opacity-70',
!clickable || 'cursor-pointer',
]"
class="flex items-stretch w-full group">
<div class="p-4 sm:px-6 flex-grow">
<div class="flex items-center justify-between">
<slot></slot>
</div>
<div v-if="hasSecondLine" class="mt-2 sm:flex sm:justify-between">
<slot name="second"></slot>
</div>
</div>
<div v-if="clickable" class="w-10 flex-shrink border-l border-shades">
<div class="flex flex-col items-center justify-center h-full">
<ChevronRightIcon class="h-4 w-4"></ChevronRightIcon>
</div>
</div>
</div>
</li>
</template>
<script>
import {ChevronRightIcon} from "@heroicons/vue/24/solid";
export default {
props: {
clickable: {
type: Boolean,
default: false,
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
item: {
type: Object,
required: true,
},
},
components: {
ChevronRightIcon
},
computed: {
hasSecondLine() {
return !!this.$slots.second;
}
}
}
</script>
<style scoped>
</style>
<template>
<nav class="w-full flex items-center justify-between border-t border-shades px-4 sm:px-0">
<div class="-mt-px flex w-0 flex-1">
<button @click="activePage > 1 ? activePage -= 1 : ''; $emit('active-page-changed', activePage)"
:class="activePage > 1 ? 'cursor-pointer hover:border-primary hover:text-primary' : 'cursor-not-allowed hover:border-shades-accent-dark hover:text-shades-accent-dark'"
class="inline-flex items-center border-t-2 border-transparent pt-4 pr-1 text-sm font-medium text-shades-accent">
<ArrowLongLeftIcon class="mr-3 h-5 w-5" aria-hidden="true"/>
Vorherige
</button>
</div>
<div class="hidden md:-mt-px md:flex">
<button
v-for="page in availablePages"
class="inline-flex items-center border-t-2 px-4 pt-4 text-sm font-medium cursor-pointer"
:class="page === activePage ? 'border-primary text-primary' : 'border-transparent text-shades-accent hover:border-primary hover:text-primary'"
@click="activePage = page; $emit('active-page-changed', activePage)"
>
{{ page }}
</button>
</div>
<div class="-mt-px flex w-0 flex-1 justify-end">
<button @click="activePage < availablePages ? activePage += 1 : ''; $emit('active-page-changed', activePage)"
:class="activePage < availablePages ? 'cursor-pointer hover:border-primary hover:text-primary cursor-pointer' : 'cursor-not-allowed hover:border-shades-accent-dark hover:text-shades-accent-dark'"
class="inline-flex items-center border-t-2 border-transparent pt-4 pl-1 text-sm font-medium text-shades-accent">
Nächste
<ArrowLongRightIcon class="ml-3 h-5 w-5" aria-hidden="true"/>
</button>
</div>
</nav>
</template>
<script>
import {ArrowLongLeftIcon, ArrowLongRightIcon} from '@heroicons/vue/20/solid'
export default {
components: {ArrowLongLeftIcon, ArrowLongRightIcon},
props: ['length', 'perSite',],
data() {
return {
activePage: 1,
localPerSite: 20,
}
},
mounted() {
if (this.perSite) this.localPerSite = this.perSite
},
computed: {
availablePages() {
return Math.ceil(this.length / this.localPerSite)
}
}
}
</script>
<template>
<div class="relative flex items-center w-full group">
<div class="absolute inset-y-0 left-0 flex items-center justify-center py-2.5 pl-1.5">
<MagnifyingGlassIcon class="h-5 w-5 text-shades-accent-dark group-hover:text-opacity-70"></MagnifyingGlassIcon>
</div>
<input :id="customId || 'search-input'" v-model="searchField" @keyup="method(this.searchField)" type="text"
name="search" :placeholder="placeholder || 'Suchen...'"
class="block form-input w-full rounded-md border-0 py-2.5 px-10 text-black shadow-sm ring-1 ring-inset ring-shades-accent focus:ring-2 focus:ring-inset focus:ring-primary focus:text-black sm:text-sm sm:leading-6"/>
<div class="absolute inset-y-0 right-0 flex py-1.5 pr-1.5">
<kbd
class="inline-flex items-center rounded border border-shades-accent px-1 font-sans text-xs text-shades-accent-dark group-hover:text-opacity-70 group-hover:border-opacity-70">⌘K</kbd>
</div>
</div>
</template>
<script>
import {MagnifyingGlassIcon} from "@heroicons/vue/24/solid";
export default {
props: ['customId', 'method', 'placeholder'],
components: {MagnifyingGlassIcon},
data() {
return {
searchField: ''
}
},
mounted() {
document.addEventListener("keydown", function (zEvent) {
if (zEvent.metaKey && zEvent.key === 'k') { // case sensitive
document.getElementById(this.customId || 'search-input').focus();
}
});
},
}
</script>
<style scoped>
</style>
<template>
<div class="px-4 sm:px-6 lg:px-8 w-full">
<div class="flow-root">
<div class="-my-2 -mx-4 sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle">
<table class="min-w-full border-spacing-0">
<thead>
<slot name="thead"></slot>
</thead>
<tbody>
<slot name="tbody"></slot>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
export default {}
</script>
<template>
<nav class="flex space-x-4 items-center w-full" :class="`justify-${justify}`">
<hr v-if="(justify === 'center' || justify === 'end') && line" class="flex-1 bg-primary-dark" style="height: 2px;">
<button @click="activateTab(tab)" v-for="tab in tabs"
class="focus:ring-2 focus:ring-primary-dark"
:class="[tab.label === activeTab.label ? 'bg-primary border-2 border-primary-dark text-shades hover:text-white hover:bg-opacity-70' : 'text-shades-dark border border-shades hover:bg-primary-dark hover:bg-opacity-50 hover:text-shades', 'rounded-md px-3 py-2 text-sm font-medium cursor-pointer']">
{{ tab.label }}
</button>
<hr v-if="(justify === 'center' || justify === 'start') && line" class="flex-1 bg-primary" style="height: 2px;">
</nav>
</template>
<script>
export default {
props: {
justify: {
type: String,
default: 'start',
required: false,
validator: (value) => {
return ['start', 'center', 'end'].includes(value)
},
},
line: {
type: Boolean,
default: false,
required: false,
},
tabs: {
type: Object,
required: true
}
},
data() {
return {
activeTab: {},
}
},
mounted() {
this.activeTab = this.tabs[0]
},
methods: {
activateTab(tab) {
this.activeTab = tab
this.$emit('tab-changed', tab)
}
}
}
</script>
<template>
<td :class="[
!border || 'border-b border-shades',
!first || '!pl-4 sm:pl-6 lg:pl-8',
!last || 'relative !pr-4 sm:pr-8 lg:pr-8',
'whitespace-nowrap px-3 py-4 text-sm'
]">
<slot></slot>
<span v-if="!hasDefaultSlot">{{ label || 'Column Data' }}</span>
</td>
</template>
<script>
export default {
props: ['first', 'label', 'last', 'border'],
computed: {
hasDefaultSlot() {
return !!this.$slots.default;
}
}
}
</script>
<template>
<th scope="col"
@click="colClicked"
:class="[
!first || '!pl-4 !pr-3 sm:pl-6 lg:pl-8',
!last || '!pr-4 !pl-3 sm:pr-6 lg:pr-8',
!sortable || 'cursor-pointer group',
topClass ? topClass : 'top-0'
]"
class="sticky z-10 border-b border-shades-dark bg-white bg-opacity-75 px-3 py-3.5 text-left text-sm font-semibold backdrop-blur backdrop-filter">
<div class="inline-flex">
<slot></slot>
<span v-if="!hasDefaultSlot">{{ label || 'Column' }}</span>
<span v-if="sortable" class="ml-2 flex-none rounded text-gray-400 group-hover:text-black">
<ArrowLongDownIcon v-if="localDirection < 0" class="h-5 w-5"/>
<ArrowLongUpIcon v-else-if="localDirection > 0" class="h-5 w-5"/>
<ArrowsUpDownIcon v-else class="h-5 w-5 opacity-30"/>
</span>
</div>
</th>
</template>
<script>
import {ArrowLongDownIcon, ArrowsUpDownIcon, ArrowLongUpIcon} from "@heroicons/vue/24/solid";
export default {
props: ['direction', 'first', 'label', 'last', 'sortable', 'topClass'],
components: {ArrowLongDownIcon, ArrowLongUpIcon, ArrowsUpDownIcon},
data() {
return {
localDirection: 0
}
},
mounted() {
this.localDirection = this.direction
},
methods: {
colClicked() {
if (!this.sortable) return
this.localDirection === 1
? this.localDirection = -1
: this.localDirection += 1
this.$emit('direction-changed', this.localDirection)
}
},
computed: {
hasDefaultSlot() {
return !!this.$slots.default;
}
},
watch: {
'direction' (val) {
this.localDirection = val
}
}
}
</script>
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
colors: {
'primary': {
DEFAULT: '#1abc9c',
'dark': '#16a085',
'accent': '#2ecc71',
'accent-dark': '#27ae60'
},
'secondary': {
DEFAULT: '#3498db',
'dark': '#2980b9'
},
'tertiary': {
DEFAULT: '#9b59b6',
'dark': '#8e44ad'
},
'midnight': {
DEFAULT: '#34495e',
'dark': '#2c3e50',
'grey': '#27272a'
},
'shades': {
DEFAULT: '#ecf0f1',
'dark': '#bdc3c7',
'accent': '#95a5a6',
'accent-dark': '#7f8c8d'
},
'success': {
DEFAULT: '#4ade80',
'dark': '#14532d'
},
'error': {
DEFAULT: '#e74c3c',
'dark': '#c0392b'
},
'warn': {
DEFAULT: '#e67e22',
'dark': '#d35400'
}
},
maxWidth: {
'screen-3xl': '1792px',
}
}
},
variants: {
extend: {},
},
plugins: [
require("@tailwindcss/forms")({
strategy: 'base', // only generate global styles
}),
],
};
<template>
<BaseCard style="height: 80vh">
<h1 class="text-4xl mb-4">Alerts</h1>
<h1 class="text-2xl my-4">Success</h1>
<BaseAlert label="Upload successful"></BaseAlert>
<h1 class="text-2xl my-4">Info</h1>
<BaseAlert variant="info" label="Upload will be started shortly"></BaseAlert>
<h1 class="text-2xl my-4">Warn</h1>
<BaseAlert variant="warn" label="Upload will be delayed"></BaseAlert>
<h1 class="text-2xl my-4">Error</h1>
<BaseAlert variant="error" label="Something went wrong while uploading"></BaseAlert>
<h1 class="text-4xl mt-6 mb-4">Badges</h1>
<h2 class="text-xl my-2">Small</h2>
<div class="flex items-center justify-start gap-x-2">
<BaseBadge v-for="color in exampleColors" :color="color" size="sm"></BaseBadge>
</div>
<h2 class="text-xl my-2">Default</h2>
<div class="flex items-center justify-start gap-x-2">
<BaseBadge v-for="color in exampleColors" :color="color"></BaseBadge>
</div>
<h2 class="text-xl my-2">Medium</h2>
<div class="flex items-center justify-start gap-x-2">
<BaseBadge v-for="color in exampleColors" :color="color" size="md"></BaseBadge>
</div>
<h2 class="text-xl my-2">Large</h2>
<div class="flex items-center justify-start gap-x-2">
<BaseBadge v-for="color in exampleColors" :color="color" size="lg"></BaseBadge>
</div>
<h1 class="text-4xl my-4">Buttons</h1>
<h1 class="text-2xl my-4">Primary</h1>
<h2 class="text-xl my-2">Small</h2>
<div class="flex items-center justify-start gap-x-2">
<BaseButton label="Base Button" size="sm"></BaseButton>
<BaseButton icon="leading" label="Leading" size="sm">
<template v-slot:leading>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton icon="trailing" label="Trailing" size="sm">
<template v-slot:trailing>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton label="Disabled" :disabled="true" size="sm"></BaseButton>
<BaseButton icon="trailing" label="Disabled Loading" :loading="true" :disabled="true" size="sm"></BaseButton>
</div>
<h2 class="text-xl my-2">Default</h2>
<div class="flex items-center justify-start gap-x-2">
<BaseButton label="Base Button"></BaseButton>
<BaseButton icon="leading" label="Leading">
<template v-slot:leading>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton icon="trailing" label="Trailing">
<template v-slot:trailing>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton label="Disabled" :disabled="true"></BaseButton>
<BaseButton icon="trailing" label="Disabled Loading" :loading="true" :disabled="true"></BaseButton>
</div>
<h2 class="text-xl my-2">Medium</h2>
<div class="flex items-center justify-start gap-x-2">
<BaseButton label="Base Button" size="md"></BaseButton>
<BaseButton icon="leading" label="Leading" size="md">
<template v-slot:leading>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton icon="trailing" label="Trailing" size="md">
<template v-slot:trailing>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton label="Disabled" :disabled="true" size="md"></BaseButton>
<BaseButton icon="trailing" label="Disabled Loading" :loading="true" :disabled="true" size="md"></BaseButton>
</div>
<h2 class="text-xl my-2">Large</h2>
<div class="flex items-center justify-start gap-x-2">
<BaseButton label="Base Button" size="lg"></BaseButton>
<BaseButton icon="leading" label="Leading" size="lg">
<template v-slot:leading>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton icon="trailing" label="Trailing" size="lg">
<template v-slot:trailing>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton label="Disabled" :disabled="true" size="lg"></BaseButton>
<BaseButton icon="trailing" label="Disabled Loading" :loading="true" :disabled="true" size="lg"></BaseButton>
</div>
<h1 class="text-2xl my-4">Secondary</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseButton label="Base Button" variant="secondary"></BaseButton>
<BaseButton icon="leading" label="Leading" variant="secondary">
<template v-slot:leading>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton icon="trailing" label="Trailing" variant="secondary">
<template v-slot:trailing>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton label="Disabled" :disabled="true" variant="secondary"></BaseButton>
<BaseButton icon="trailing" label="Disabled Loading" :loading="true" :disabled="true"
variant="secondary"></BaseButton>
</div>
<h1 class="text-2xl my-4">Tertiary</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseButton label="Base Button" variant="tertiary"></BaseButton>
<BaseButton icon="leading" label="Leading" variant="tertiary">
<template v-slot:leading>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton icon="trailing" label="Trailing" variant="tertiary">
<template v-slot:trailing>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton label="Disabled" :disabled="true" variant="tertiary"></BaseButton>
<BaseButton icon="trailing" label="Disabled Loading" :loading="true" :disabled="true"
variant="tertiary"></BaseButton>
</div>
<h1 class="text-2xl my-4">Warn</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseButton label="Base Button" variant="warn"></BaseButton>
<BaseButton icon="leading" label="Leading" variant="warn">
<template v-slot:leading>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton icon="trailing" label="Trailing" variant="warn">
<template v-slot:trailing>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton label="Disabled" :disabled="true" variant="warn"></BaseButton>
<BaseButton icon="trailing" label="Disabled Loading" :loading="true" :disabled="true"
variant="warn"></BaseButton>
</div>
<h1 class="text-2xl my-4">Error</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseButton label="Base Button" variant="error"></BaseButton>
<BaseButton icon="leading" label="Leading" variant="error">
<template v-slot:leading>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton icon="trailing" label="Trailing" variant="error">
<template v-slot:trailing>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton label="Disabled" :disabled="true" variant="error"></BaseButton>
<BaseButton icon="trailing" label="Disabled Loading" :loading="true" :disabled="true"
variant="error"></BaseButton>
</div>
<h1 class="text-2xl my-4">Outline</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseButton label="Base Button" variant="outline"></BaseButton>
<BaseButton icon="leading" label="Leading" variant="outline">
<template v-slot:leading>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton icon="trailing" label="Trailing" variant="outline">
<template v-slot:trailing>
<CheckCircleIcon aria-hidden="true"/>
</template>
</BaseButton>
<BaseButton label="Disabled" :disabled="true" variant="outline"></BaseButton>
<BaseButton icon="trailing" label="Disabled Loading" :loading="true" :disabled="true"
variant="outline"></BaseButton>
</div>
<h1 class="text-4xl mt-6 mb-4">Drag 'n' Drop - XLIFF</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseDragDrop class="h-full w-full"></BaseDragDrop>
</div>
<h1 class="text-4xl mb-4">Form</h1>
<div class="flex flex-wrap items-center justify-start gap-4">
<label class="form-input-wrapper">
<span class="form-label">Input (text)</span>
<input
type="text"
class="form-input "
placeholder="john@example.com"
/>
</label>
<label class="form-input-wrapper">
<span class="form-label" readonly>Input (text)</span>
<input
type="text"
class="form-input "
placeholder="john@example.com"
value="john@example.com"
readonly
/>
</label>
<label class="form-input-wrapper">
<span class="form-label" disabled>Input (text)</span>
<input
type="text"
class="form-input "
placeholder="john@example.com"
disabled
/>
</label>
<label class="form-input-wrapper form-has-errors">
<span class="form-label">Input (text)</span>
<input
type="text"
class="form-input "
placeholder="john@example.com"
/>
<ul class="form-errors">
<li class="form-error-msg" v-for="i in ['Diese Feld ist erforderlich', 'Keine gültige E-Mail']">{{ i }}</li>
</ul>
</label>
<label class="form-input-wrapper">
<span class="form-label">Input (email)</span>
<input
type="email"
class="form-input "
placeholder="john@example.com"
/>
</label>
<label class="form-input-wrapper">
<span class="form-label">Input (email, multiple)</span>
<input
type="email"
multiple
class="form-input "
placeholder="john@example.com"
/>
</label>
<label class="form-input-wrapper">
<span class="form-label">Input (password)</span>
<input
type="password"
class="form-input "
placeholder="john@example.com"
/>
</label>
<label class="form-input-wrapper">
<span class="form-label">Input (date)</span>
<input type="date" class="form-input "/>
</label>
<label class="form-input-wrapper">
<span class="form-label">Input (datetime-local)</span>
<input type="datetime-local" class="form-input "/>
</label>
<label class="form-input-wrapper">
<span class="form-label">Input (month)</span>
<input type="month" class="form-input "/>
</label>
<label class="form-input-wrapper">
<span class="form-label">Input (number)</span>
<input type="number" class="form-input "/>
</label>
<label class="form-input-wrapper">
<span class="form-label">Input (search)</span>
<input type="search" class="form-input "/>
</label>
<label class="form-input-wrapper">
<span class="form-label">Input (time)</span>
<input type="time" class="form-input "/>
</label>
<label class="form-input-wrapper">
<span class="form-label">Input (week)</span>
<input type="week" class="form-input "/>
</label>
<label class="form-input-wrapper">
<span class="form-label">Input (tel)</span>
<input
type="tel"
multiple
class="form-input "
placeholder="john@example.com"
/>
</label>
<label class="form-input-wrapper">
<span class="form-label">Input (url)</span>
<input
type="url"
multiple
class="form-input "
placeholder="john@example.com"
/>
</label>
<label class="form-input-wrapper">
<span class="form-label">Select</span>
<select class="form-select block w-full mt-1">
<option>Option 1</option>
<option>Option 2</option>
</select>
</label>
<label class="form-input-wrapper">
<span class="form-label">Select (multiple)</span>
<select class="form-multiselect block w-full mt-1" multiple>
<option>Option 1</option>
<option>Option 2</option>
<option>Option 3</option>
<option>Option 4</option>
<option>Option 5</option>
</select>
</label>
<label class="form-input-wrapper">
<span class="form-label">Textarea</span>
<textarea
class="form-textarea h-24"
rows="3"
placeholder="Enter some long form content."
></textarea>
</label>
<fieldset class="block">
<div class="mt-2">
<div>
<label class="form-box-wrapper">
<input class="form-checkbox" type="checkbox" checked/>
<span class="ml-2">Option 1</span>
</label>
</div>
<div>
<label class="form-box-wrapper">
<input class="form-checkbox" type="checkbox"/>
<span class="ml-2">Option 2</span>
</label>
</div>
<div>
<label class="form-box-wrapper">
<input class="form-checkbox" type="checkbox"/>
<span class="ml-2">Option 3</span>
</label>
</div>
</div>
</fieldset>
<fieldset class="block">
<div class="mt-2">
<div>
<label class="form-box-wrapper">
<input class="form-radio" type="radio" checked name="radio-direct" value="1"/>
<span class="ml-2">Option 1</span>
</label>
</div>
<div>
<label class="form-box-wrapper">
<input class="form-radio" type="radio" name="radio-direct" value="2"/>
<span class="ml-2">Option 2</span>
</label>
</div>
<div>
<label class="form-box-wrapper">
<input class="form-radio" type="radio" name="radio-direct" value="3"/>
<span class="ml-2">Option 3</span>
</label>
</div>
</div>
</fieldset>
</div>
<h1 class="text-4xl mt-6 mb-4">Lists</h1>
<h1 class="text-2xl my-4">Simple - Two Column</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseList>
<BaseListItem v-for="position in positions" :item="position">
<p class="truncate text-sm font-semibold">{{ position.title }}</p>
<div class="ml-2 flex flex-shrink-0">
<p class="inline-flex rounded-full bg-green-100 px-2 text-xs font-semibold leading-5 group-hover:bg-white group-hover:text-black">
{{ position.type }}</p>
</div>
</BaseListItem>
</BaseList>
</div>
<h1 class="text-2xl my-4">Simple - Two Column - Disabled</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseList>
<BaseListItem v-for="position in positions" :item="position" :disabled="true">
<p class="truncate text-sm font-semibold">{{ position.title }}</p>
<div class="ml-2 flex flex-shrink-0">
<p class="inline-flex rounded-full bg-white text-black px-2 text-xs font-semibold leading-5">
{{ position.type }}</p>
</div>
</BaseListItem>
</BaseList>
</div>
<h1 class="text-2xl my-4">Simple - Multi-Column</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseList>
<BaseListItem v-for="position in positions" :item="position">
<p class="text-sm font-semibold">{{ position.title }}</p>
<p class="text-sm font-semibold">{{ position.title }}</p>
<p class="text-sm font-semibold">{{ position.title }}</p>
<div class="ml-2 flex flex-shrink-0">
<p class="inline-flex rounded-full bg-green-100 px-2 text-xs font-semibold leading-5 group-hover:bg-white group-hover:text-black">
{{ position.type }}</p>
</div>
</BaseListItem>
</BaseList>
</div>
<h1 class="text-2xl my-4">Two Row - Two Column - Clickable</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseList>
<BaseListItem v-for="position in positions" :clickable="true" :item="position">
<p class="truncate text-sm font-semibold">{{ position.title }}</p>
<div class="ml-2 flex flex-shrink-0">
<p class="inline-flex rounded-full bg-green-100 px-2 text-xs font-semibold leading-5 group-hover:bg-white group-hover:text-black">
{{ position.type }}</p>
</div>
<template v-slot:second>
<div class="sm:flex font-light">
<p class="flex items-center text-sm">
<UsersIcon class="mr-1.5 h-5 w-5 flex-shrink-0" aria-hidden="true"/>
{{ position.department }}
</p>
<p class="mt-2 flex items-center text-sm sm:mt-0 sm:ml-6">
<MapPinIcon class="mr-1.5 h-5 w-5 flex-shrink-0" aria-hidden="true"/>
{{ position.location }}
</p>
</div>
<div class="mt-2 flex items-center text-sm sm:mt-0">
<CalendarIcon class="mr-1.5 h-5 w-5 flex-shrink-0" aria-hidden="true"/>
<p>
Closing on
{{ ' ' }}
<time :datetime="position.closeDate">{{ position.closeDateFull }}</time>
</p>
</div>
</template>
</BaseListItem>
</BaseList>
</div>
<h1 class="text-4xl mt-6 mb-4">Menus</h1>
<h1 class="text-2xl my-4">Simple</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseDropDown :levels="simpleLevels" label="Simple">
</BaseDropDown>
<BaseDropDown :levels="simpleLevels" class="ml-8">
<div
class="cursor-pointer rounded-full text-gray-600 text-xs p-1 h-8 w-8 flex items-center justify-center uppercase">
<EllipsisHorizontalCircleIcon></EllipsisHorizontalCircleIcon>
</div>
</BaseDropDown>
</div>
<h1 class="text-2xl my-4">Multilevel - Links nach Rechts</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseDropDown :levels="levels" label="Multilevel" ref="levelChild"></BaseDropDown>
<p v-if="levelDemonstration">Level click -> {{ levelDemonstration }}</p>
</div>
<h1 class="text-2xl my-4">Multilevel - Rechts nach Links</h1>
<div class="flex items-center justify-end gap-x-2">
<BaseDropDown :levels="levels" label="Multilevel" open-to="left"></BaseDropDown>
</div>
<h1 class="text-4xl mt-6 mb-4">Tables</h1>
<h1 class="text-2xl my-4">Simple - Sticky Header</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseTable>
<template v-slot:thead>
<tr>
<BaseTH :first="true">Name</BaseTH>
<BaseTH class="hidden sm:table-cell">Title</BaseTH>
<BaseTH class="hidden lg:table-cell">Email</BaseTH>
<BaseTH>Role</BaseTH>
<BaseTH :last="true"><span class="sr-only">Edit</span></BaseTH>
</tr>
</template>
<template v-slot:tbody>
<tr v-for="(person, personIdx) in peoples" :key="personIdx + person.name + 'x'"
class="group hover:bg-primary hover:text-white hover:bg-opacity-70 divide-x divide-shades hover:divide-white">
<BaseTD :border="personIdx !== people.length - 1" :first="true">{{ person.name }}</BaseTD>
<BaseTD :border="personIdx !== people.length - 1" class="hidden sm:table-cell">{{ person.title }}</BaseTD>
<BaseTD :border="personIdx !== people.length - 1" class="hidden lg:table-cell">{{ person.email }}</BaseTD>
<BaseTD :border="personIdx !== people.length - 1">{{ person.role }}</BaseTD>
<BaseTD :border="personIdx !== people.length - 1" :last="true">
<a href="#" class="text-indigo-600 group-hover:text-white">Edit</a>
</BaseTD>
</tr>
</template>
</BaseTable>
</div>
<h1 class="text-2xl my-4">Simple - Sortable Header - Search</h1>
<div class="flex items-center justify-center mb-4">
<div class="w-1/2">
<BaseSearchField :method="searchPeople"></BaseSearchField>
</div>
</div>
<div class="flex items-center justify-start gap-x-2">
<BaseTable>
<template v-slot:thead>
<tr>
<BaseTH v-for="(col, index) in peopleSortColumns"
:class="col.class"
:key="index + col.label"
:direction="col.direction"
@direction-changed="adjustSorting($event, col)"
:first="index === 0"
:last="index === peopleSortColumns.length -1"
:sortable="col.sortable">
<span v-html="col.label"></span>
</BaseTH>
</tr>
</template>
<template v-slot:tbody>
<template v-for="(person, personIdx) in people"
:key="personIdx + person.name">
<tr
v-if="!person.hide"
class="group hover:bg-primary hover:text-white hover:bg-opacity-70 divide-x divide-shades hover:divide-white">
<BaseTD :border="personIdx !== people.length - 1" :first="true">{{ person.name }}</BaseTD>
<BaseTD :border="personIdx !== people.length - 1" class="hidden sm:table-cell">{{ person.title }}</BaseTD>
<BaseTD :border="personIdx !== people.length - 1" class="hidden lg:table-cell">{{ person.email }}</BaseTD>
<BaseTD :border="personIdx !== people.length - 1">{{ person.role }}</BaseTD>
<BaseTD :border="personIdx !== people.length - 1" :last="true">
<a href="#" class="text-indigo-600 group-hover:text-white">Edit</a>
</BaseTD>
</tr>
</template>
</template>
</BaseTable>
</div>
<h1 class="text-2xl my-4">Pagination</h1>
<div class="flex items-center justify-start gap-x-2">
<BasePagination :length="100"></BasePagination>
</div>
<h1 class="text-4xl mt-6 mb-4">Tabs</h1>
<h1 class="text-2xl my-4">Start</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseTabs :tabs="tabs"></BaseTabs>
</div>
<h1 class="text-2xl my-4">Start - With Line</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseTabs :tabs="tabs" justify="start" :line="true"></BaseTabs>
</div>
<h1 class="text-2xl my-4">Center</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseTabs :tabs="tabs" justify="center"></BaseTabs>
</div>
<h1 class="text-2xl my-4">Center - With Lines</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseTabs :tabs="tabs" justify="center" :line="true"></BaseTabs>
</div>
<h1 class="text-2xl my-4">End</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseTabs :tabs="tabs" justify="end"></BaseTabs>
</div>
<h1 class="text-2xl my-4">End - With Line</h1>
<div class="flex items-center justify-start gap-x-2">
<BaseTabs :tabs="tabs" justify="end" :line="true"></BaseTabs>
</div>
</BaseCard>
</template>
<script>
import BaseAlert from "./Includes/BaseAlert";
import BaseBadge from "./Includes/BaseBadge";
import BaseButton from "./Includes/BaseButton";
import BaseCard from "./Includes/BaseCard";
import {CalendarIcon, CheckCircleIcon, MagnifyingGlassIcon, MapPinIcon, UsersIcon} from "@heroicons/vue/24/solid";
import BaseList from "./Includes/BaseList";
import BaseListItem from "./Includes/BaseListItem";
import BaseTabs from "./Includes/BaseTabs";
import BaseTable from "./Includes/BaseTable";
import BaseTH from "./Includes/BaseTH";
import BaseTD from "./Includes/BaseTD";
import BasePagination from "./Includes/BasePagination";
import BaseDropDown from "./Includes/BaseDropDown";
import {EllipsisHorizontalCircleIcon} from "@heroicons/vue/24/outline";
import BaseSearchField from "./Includes/BaseSearchField";
import BaseDragDrop from "./Includes/BaseDragDrop";
export default {
components: {
BaseAlert,
BaseBadge,
BaseButton,
BaseCard,
BaseDragDrop,
BaseDropDown,
BaseList,
BaseListItem,
BasePagination,
BaseTable,
BaseTD,
BaseTH,
BaseTabs,
BaseSearchField,
CheckCircleIcon,
UsersIcon,
MapPinIcon,
CalendarIcon,
EllipsisHorizontalCircleIcon,
MagnifyingGlassIcon
},
data() {
return {
exampleColors: [
'gray',
'red',
'yellow',
'green',
'blue',
'indigo',
'purple',
'pink',
],
peoples: [
{name: 'Lindsay Walton', title: 'Front-end Developer', email: 'lindsay.walton@example.com', role: 'Member'},
{name: 'Markus Donton', title: 'Back-end Developer', email: 'markus.donton@example.com', role: 'Admin'},
{name: 'Lindsay Walton', title: 'Front-end Developer', email: 'lindsay.walton@example.com', role: 'Member'},
{name: 'Markus Donton', title: 'Back-end Developer', email: 'markus.donton@example.com', role: 'Admin'},
{name: 'Lindsay Walton', title: 'Front-end Developer', email: 'lindsay.walton@example.com', role: 'Member'},
{name: 'Markus Donton', title: 'Back-end Developer', email: 'markus.donton@example.com', role: 'Admin'},
{name: 'Lindsay Walton', title: 'Front-end Developer', email: 'lindsay.walton@example.com', role: 'Member'},
{name: 'Markus Donton', title: 'Back-end Developer', email: 'markus.donton@example.com', role: 'Admin'},
],
people: [
{
hide: false,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
},
{
hide: false,
name: 'Bernd',
title: 'Full-Stack Developer',
email: 'Bernd@example.de',
role: 'Bernd'
},
{
hide: false,
name: 'Markus Donton',
title: 'Back-end Developer',
email: 'markus.donton@example.com',
role: 'Admin'
},
{
hide: false,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
},
{
hide: false,
name: 'Markus Donton',
title: 'Back-end Developer',
email: 'markus.donton@example.com',
role: 'Admin'
},
{
hide: false,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
},
{
hide: false,
name: 'Markus Donton',
title: 'Back-end Developer',
email: 'markus.donton@example.com',
role: 'Admin'
},
{
hide: false,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
},
{
hide: false,
name: 'Markus Donton',
title: 'Back-end Developer',
email: 'markus.donton@example.com',
role: 'Admin'
},
],
peopleSortColumns: [
{label: 'Name', field: 'name', sortable: true, direction: 1, class: ''},
{label: 'Titel', field: 'title', sortable: true, direction: 0, class: 'hidden sm:table-cell'},
{label: 'E-Mail', field: 'email', sortable: true, direction: 0, class: 'hidden lg:table-cell'},
{label: 'Role', field: 'role', sortable: true, direction: 0, class: ''},
{label: `<span class="sr-only">Edit</span>`, sortable: false, direction: 0, class: ''},
],
searchField: '',
positions: [
{
id: 1,
title: 'Back End Developer',
type: 'Full-time',
location: 'Remote',
department: 'Engineering',
closeDate: '2020-01-07',
closeDateFull: 'January 7, 2020',
},
{
id: 2,
title: 'Front End Developer',
type: 'Full-time',
location: 'Remote',
department: 'Engineering',
closeDate: '2020-01-07',
closeDateFull: 'January 7, 2020',
},
{
id: 3,
title: 'User Interface Designer',
type: 'Full-time',
location: 'Remote',
department: 'Design',
closeDate: '2020-01-14',
closeDateFull: 'January 14, 2020',
},
],
tabs: [{label: 'Tab 1'}, {label: 'Tab 2'}, {label: 'Tab 3'}, {label: 'Tab 4'}],
levelDemonstration: '',
simpleLevels: [{
label: "Erstes Level mit Icon",
icon: "BookOpenIcon",
}, {
label: "Zweites Level mit Icon und Trenner",
icon: "BookOpenIcon",
divider: true
}, {
label: "Drittes Level mit Icon",
icon: "BookOpenIcon",
}, {
label: "Viertes Level mit Icon",
icon: "BookOpenIcon",
}],
levels: [
{
label: "Erstes Level mit Icon",
icon: "BookOpenIcon",
method: () => this.showLevel("1"),
children: [
{
label: "1-1",
method: () => this.showLevel("1-1"),
children: [
{label: "1-1-1 mit Trenner", divider: true, method: () => this.showLevel("1-1-1")},
{
label: "1-1-2", method: () => this.showLevel("1-1-2"),
}
]
},
{label: "1-2", method: () => this.showLevel("1-2")}
]
},
{label: "Zweites Level mit Trenner", divider: true, method: () => this.showLevel("2")},
{label: "Drittes Level", method: () => this.showLevel("3")},
{
label: "Viertes Level", method: () => this.showLevel("4"),
children: [
{label: "4-1", method: () => this.showLevel("4-1")},
{
label: "4-2 mit Icon", icon: "BookOpenIcon", method: () => this.showLevel("4-2")
}
]
},
]
}
},
methods: {
adjustSorting(direction, column) {
this.peopleSortColumns.forEach((col) => {
if (col.label === column.label) {
col.direction = direction
} else {
col.direction = 0
}
})
this.people.sort((a, b) => {
const textA = a[column.field] ? a[column.field].toUpperCase() : 'ZZZZZZ'
const textB = b[column.field] ? b[column.field].toUpperCase() : 'ZZZZZZ'
return textA < textB
? direction * -1
: textA > textB
? direction : 0
})
},
searchPeople(field) {
let compare = field.toUpperCase()
this.people.forEach((p) => {
p.hide = p.name.toUpperCase().indexOf(compare) <= -1
&& p.title.toUpperCase().indexOf(compare) <= -1
&& p.email.toUpperCase().indexOf(compare) <= -1
&& p.role.toUpperCase().indexOf(compare) <= -1;
})
},
showLevel(name) {
this.levelDemonstration = name
this.$refs.levelChild.hideMe()
}
}
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment