mirror of
https://github.com/ivabus/gui
synced 2025-04-23 14:07:14 +03:00
* #301 init home page with side nav * #301 filter by state and sort * #301 update package cards layout --------- Co-authored-by: neil <neil@neils-MacBook-Pro.local>
This commit is contained in:
parent
b43a64566d
commit
c5a3a72948
13 changed files with 307 additions and 219 deletions
|
@ -49,7 +49,8 @@
|
|||
link={`/packages/${pkg.slug}`}
|
||||
ctaLabel={getCTALabel(pkg.state)}
|
||||
progessLoading={+fakeLoadingProgress.toFixed(2)}
|
||||
ctaType={PackageStates.INSTALLED === pkg.state ? "ghost" : "plain"}
|
||||
ctaType="plain"
|
||||
ctaColor={PackageStates.INSTALLED === pkg.state ? "green" : "secondary"}
|
||||
onClickCTA={async () => {
|
||||
fakeLoadingProgress = 1;
|
||||
startFakeLoader();
|
||||
|
|
|
@ -9,23 +9,18 @@
|
|||
|
||||
import { installPackage } from '@native';
|
||||
import { trackInstall, trackInstallFailed } from '$libs/analytics';
|
||||
import SortingButtons from '$components/search-packages/sorting-buttons.svelte';
|
||||
export let title = 'Packages';
|
||||
|
||||
let pkgNeedsUpdateCount = 0;
|
||||
const { packages: allPackages } = packagesStore;
|
||||
|
||||
export let tab: "ALL" | "INSTALLED" | "INSTALLED_WITH_UPDATES" = "ALL";
|
||||
export let stateFilters: {[key: string]: boolean};
|
||||
export let sortBy: "popularity" | "most recent" = 'popularity';
|
||||
export let sortDirection: 'asc' | 'desc' = 'desc';
|
||||
|
||||
let sortBy = 'popularity';
|
||||
let sortDirection: 'asc' | 'desc' = 'desc';
|
||||
const loadMore = 12;
|
||||
const loadMore = 9;
|
||||
let limit = loadMore;
|
||||
|
||||
let packages: GUIPackage[] = [];
|
||||
|
||||
const setPackages = (pkgs: GUIPackage[]) => {
|
||||
pkgNeedsUpdateCount = pkgs.filter((p) => p.state === PackageStates.NEEDS_UPDATE).length;
|
||||
const sortedPackages = pkgs.sort((a, b) => {
|
||||
$: filterExists = Object.keys(stateFilters).some((k) => stateFilters[k]);
|
||||
$: packages = $allPackages
|
||||
.sort((a, b) => {
|
||||
if (sortBy === 'popularity') {
|
||||
const aPop = +a.dl_count + a.installs;
|
||||
const bPop = +b.dl_count + b.installs;
|
||||
|
@ -36,100 +31,63 @@
|
|||
const bDate = new Date(b.last_modified);
|
||||
return sortDirection === 'asc' ? +aDate - +bDate : +bDate - +aDate;
|
||||
}
|
||||
})
|
||||
.filter((pkg) => {
|
||||
if (!filterExists || pkg.state === PackageStates.INSTALLING) return true;
|
||||
return stateFilters[pkg.state];
|
||||
});
|
||||
|
||||
const filteredStates = [
|
||||
PackageStates.NEEDS_UPDATE
|
||||
];
|
||||
|
||||
switch (tab) {
|
||||
case "INSTALLED":
|
||||
case "INSTALLED_WITH_UPDATES":
|
||||
if (tab === "INSTALLED") filteredStates.push(PackageStates.INSTALLED);
|
||||
packages = sortedPackages.filter((p) => filteredStates.includes(p.state!));
|
||||
break;
|
||||
case "ALL":
|
||||
default:
|
||||
packages = sortedPackages;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
packagesStore.subscribe(setPackages);
|
||||
|
||||
const onSort = (opt: string, dir: 'asc' | 'desc') => {
|
||||
sortBy = opt;
|
||||
sortDirection = dir;
|
||||
setPackages(packages);
|
||||
};
|
||||
|
||||
const switchTab = (nextTab: "ALL" | "INSTALLED" | "INSTALLED_WITH_UPDATES") => {
|
||||
tab = nextTab;
|
||||
setPackages($packagesStore);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<header class="flex items-center justify-between my-4">
|
||||
<h1 class="text-primary text-4xl font-bold">{title}</h1>
|
||||
<!-- <header class="flex items-center justify-between z-50 w-full absolute">
|
||||
<h1 class="text-primary text-4xl font-bold">{$t("home.all-packages")}</h1>
|
||||
<div class="flex">
|
||||
<section class="border border-gray mr-2 rounded-sm h-10 text-gray font-thin flex">
|
||||
<button on:click={() => switchTab("ALL")} class={`px-2 ${tab === "ALL" && "active"}`}>All packages</button>
|
||||
<button on:click={() => switchTab("INSTALLED")} class={`px-2 ${tab === "INSTALLED" && "active"}`}>installed only</button>
|
||||
{#if pkgNeedsUpdateCount}
|
||||
<button on:click={() => switchTab("INSTALLED_WITH_UPDATES")} class={`px-2 ${tab === "INSTALLED_WITH_UPDATES" && "active"}`}>
|
||||
<div class="flex justify-center align-middle">
|
||||
<div>updates</div>
|
||||
<div class="bg-red text-white bg-[red] rounded-sm text-xs h-6 leading-6 px-1 ml-2">{pkgNeedsUpdateCount}</div>
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
</section>
|
||||
<section class="border-gray h-10 w-48 border rounded-sm">
|
||||
<SortingButtons {onSort} />
|
||||
</section>
|
||||
</div>
|
||||
</header>
|
||||
<ul class="grid grid-cols-3 bg-black">
|
||||
{#if packages.length > 0}
|
||||
{#each packages as pkg, index}
|
||||
{#if index < limit}
|
||||
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
|
||||
<Package
|
||||
{pkg}
|
||||
onClick={async () => {
|
||||
try {
|
||||
pkg.state = PackageStates.INSTALLING;
|
||||
await installPackage(pkg);
|
||||
trackInstall(pkg.full_name);
|
||||
pkg.state = PackageStates.INSTALLED;
|
||||
|
||||
packagesStore.updatePackage(pkg.full_name, {
|
||||
state: PackageStates.INSTALLED, // this would also mean its the latest version
|
||||
});
|
||||
} catch (error) {
|
||||
let message = 'Unknown Error'
|
||||
if (error instanceof Error) message = error.message
|
||||
trackInstallFailed(pkg.full_name, message || "unknown");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
{#each Array(9) as _}
|
||||
<section class="h-50 border-gray border p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/each}
|
||||
</header> -->
|
||||
<div>
|
||||
<ul class="grid grid-cols-3 gap-2 bg-black">
|
||||
{#if packages.length > 0}
|
||||
{#each packages as pkg, index}
|
||||
{#if index < limit}
|
||||
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
|
||||
<Package
|
||||
{pkg}
|
||||
onClick={async () => {
|
||||
try {
|
||||
pkg.state = PackageStates.INSTALLING;
|
||||
await installPackage(pkg);
|
||||
trackInstall(pkg.full_name);
|
||||
pkg.state = PackageStates.INSTALLED;
|
||||
|
||||
packagesStore.updatePackage(pkg.full_name, {
|
||||
state: PackageStates.INSTALLED, // this would also mean its the latest version
|
||||
});
|
||||
} catch (error) {
|
||||
let message = 'Unknown Error'
|
||||
if (error instanceof Error) message = error.message
|
||||
trackInstallFailed(pkg.full_name, message || "unknown");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
{#each Array(9) as _}
|
||||
<section class="h-50 border-gray border p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
{#if limit < packages.length }
|
||||
<footer class="w-full flex border border-gray h-16">
|
||||
<button class="flex-grow h-16" on:click={() => limit += loadMore }>show more</button>
|
||||
</footer>
|
||||
{/if}
|
||||
</ul>
|
||||
{#if limit < packages.length }
|
||||
<footer class="w-full flex border border-gray h-16">
|
||||
<button class="flex-grow h-16" on:click={() => limit += loadMore }>show more</button>
|
||||
</footer>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
button {
|
||||
|
|
|
@ -2,25 +2,27 @@
|
|||
import '$appcss';
|
||||
import { t } from '$libs/translations';
|
||||
|
||||
export let onSort: (opt: string, dir: 'asc' | 'desc') => void;
|
||||
|
||||
let sortBy = 'popularity';
|
||||
type SortOption = "popularity" | "most recent";
|
||||
export let onSort: (opt: SortOption, dir: 'asc' | 'desc') => void;
|
||||
|
||||
let sortBy: SortOption = "popularity";
|
||||
let sortDirection: 'asc' | 'desc' = 'desc';
|
||||
|
||||
const sortOptions = ['popularity', 'most recent'];
|
||||
const sortOptions: SortOption[] = ["popularity", "most recent"];
|
||||
|
||||
const optionLabels = {
|
||||
[sortOptions[0]]: $t("sorting.popularity"),
|
||||
[sortOptions[1]]: $t("sorting.most-recent")
|
||||
}
|
||||
|
||||
const setSortBy = (opt: string) => {
|
||||
const setSortBy = (opt: SortOption) => {
|
||||
sortBy = opt;
|
||||
if (onSort) {
|
||||
onSort(sortBy, sortDirection);
|
||||
}
|
||||
};
|
||||
const setSortDir = (opt: string, dir: 'asc' | 'desc') => {
|
||||
const setSortDir = (opt: SortOption, dir: 'asc' | 'desc') => {
|
||||
sortDirection = dir;
|
||||
setSortBy(opt);
|
||||
};
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
<header class="text-primary p-4 text-lg">
|
||||
Top Package Results ({packages.length})
|
||||
</header>
|
||||
<ul class="grid grid-cols-3">
|
||||
<ul class="grid grid-cols-3 gap-2">
|
||||
{#if packages.length > 0}
|
||||
{#each packages as pkg}
|
||||
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
|
||||
|
|
|
@ -10,14 +10,14 @@ import semverCompare from "semver/functions/compare";
|
|||
|
||||
export default function initPackagesStore() {
|
||||
let initialized = false;
|
||||
const { subscribe, set, update } = writable<GUIPackage[]>([]);
|
||||
const packages: GUIPackage[] = [];
|
||||
const packages = writable<GUIPackage[]>([]);
|
||||
// const packages: GUIPackage[] = [];
|
||||
let packagesIndex: Fuse<GUIPackage>;
|
||||
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
getPackages().then((pkgs) => {
|
||||
set(pkgs);
|
||||
packages.set(pkgs);
|
||||
packagesIndex = new Fuse(pkgs, {
|
||||
keys: ["name", "full_name", "desc", "categories"]
|
||||
});
|
||||
|
@ -30,10 +30,8 @@ export default function initPackagesStore() {
|
|||
});
|
||||
}
|
||||
|
||||
subscribe((v) => packages.push(...v));
|
||||
|
||||
const updatePackage = (full_name: string, props: Partial<GUIPackage>) => {
|
||||
update((pkgs) => {
|
||||
packages.update((pkgs) => {
|
||||
const i = pkgs.findIndex((pkg) => pkg.full_name === full_name);
|
||||
if (i >= 0) {
|
||||
pkgs[i] = {
|
||||
|
@ -77,26 +75,35 @@ To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
|
|||
|
||||
const syncPackageBottlesAndState = async (pkgName: string) => {
|
||||
const bottles = await getPackageBottles(pkgName);
|
||||
const pkg = packages.find((p) => p.full_name === pkgName);
|
||||
|
||||
const availableVersions = bottles
|
||||
.map(({ version }) => version)
|
||||
.sort((a, b) => semverCompare(b, a));
|
||||
packages.update((pkgs) => {
|
||||
const i = pkgs.findIndex((pkg) => pkg.full_name === pkgName);
|
||||
if (i >= 0) {
|
||||
const pkg = pkgs[i];
|
||||
|
||||
const installedVersions = pkg?.installed_versions?.sort((a, b) => semverCompare(b, a)) || [];
|
||||
const availableVersions = bottles
|
||||
.map(({ version }) => version)
|
||||
.sort((a, b) => semverCompare(b, a));
|
||||
|
||||
updatePackage(pkgName, {
|
||||
available_versions: availableVersions,
|
||||
state:
|
||||
availableVersions[0] === installedVersions[0]
|
||||
? PackageStates.INSTALLED
|
||||
: PackageStates.NEEDS_UPDATE
|
||||
const installedVersions =
|
||||
pkg?.installed_versions?.sort((a, b) => semverCompare(b, a)) || [];
|
||||
|
||||
pkgs[i] = {
|
||||
...pkg,
|
||||
available_versions: availableVersions,
|
||||
state:
|
||||
availableVersions[0] === installedVersions[0]
|
||||
? PackageStates.INSTALLED
|
||||
: PackageStates.NEEDS_UPDATE
|
||||
};
|
||||
}
|
||||
return pkgs;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
packages,
|
||||
subscribe,
|
||||
subscribe: packages.subscribe,
|
||||
search: async (term: string, limit = 5): Promise<GUIPackage[]> => {
|
||||
if (!term || !packagesIndex) return [];
|
||||
// TODO: if online, use algolia else use Fuse
|
||||
|
@ -105,7 +112,7 @@ To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
|
|||
return matchingPackages;
|
||||
},
|
||||
subscribeToPackage: (slug: string, cb: (pkg: GUIPackage) => void) => {
|
||||
subscribe((pkgs) => {
|
||||
packages.subscribe((pkgs) => {
|
||||
const foundPackage = pkgs.find((p) => p.slug === slug) as GUIPackage;
|
||||
if (foundPackage) {
|
||||
cb(foundPackage);
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"store-search-placeholder": "type to search",
|
||||
"search": "search",
|
||||
"home": {
|
||||
"all-packages": "All packages",
|
||||
"discover-title": "DISCOVER",
|
||||
"asset-title": "ASSET TYPE",
|
||||
"tutorials-title": "TUTORIALS",
|
||||
|
@ -63,7 +64,7 @@
|
|||
},
|
||||
"view-all": "view all",
|
||||
"sorting": {
|
||||
"label": "Filters",
|
||||
"label": "Sort by",
|
||||
"popularity": "Most popular",
|
||||
"most-recent": "Most recent"
|
||||
},
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
import type { Package, Developer } from "@tea/ui/types";
|
||||
|
||||
export enum PackageStates {
|
||||
AVAILABLE,
|
||||
INSTALLED,
|
||||
INSTALLING,
|
||||
UNINSTALLED,
|
||||
NEEDS_UPDATE
|
||||
AVAILABLE = "AVAILABLE",
|
||||
INSTALLED = "INSTALLED",
|
||||
INSTALLING = "INSTALLING",
|
||||
UNINSTALLED = "UNINSTALLED",
|
||||
NEEDS_UPDATE = "NEEDS_UPDATE"
|
||||
}
|
||||
|
||||
export type GUIPackage = Package & {
|
||||
|
|
|
@ -63,8 +63,8 @@
|
|||
<div class="content">
|
||||
<TeaUpdate />
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot />
|
||||
<div class="content p-2">
|
||||
<slot/>
|
||||
</div>
|
||||
<SearchPopupResults />
|
||||
</section>
|
||||
|
@ -85,21 +85,6 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1440px) {
|
||||
figure {
|
||||
background-size: cover;
|
||||
background-repeat: repeat-y;
|
||||
}
|
||||
.content {
|
||||
padding: 0vw 3.6vw !important;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 1440px) {
|
||||
.content {
|
||||
padding: 0vw 3.33vw;
|
||||
}
|
||||
}
|
||||
|
||||
slot {
|
||||
z-index: 1;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,92 @@
|
|||
<!-- home / discover / welcome page -->
|
||||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import { t } from '$libs/translations';
|
||||
import { t } from '$libs/translations';
|
||||
import { navStore, packagesStore, notificationStore } from '$libs/stores';
|
||||
import Packages from '$components/packages/packages.svelte';
|
||||
import Resources from '$components/resources/resources.svelte';
|
||||
import Checkbox from "@tea/ui/checkbox/checkbox.svelte";
|
||||
import { PackageStates } from '$libs/types';
|
||||
import SortingButtons from "$components/search-packages/sorting-buttons.svelte";
|
||||
|
||||
const { sideNavOpen } = navStore;
|
||||
|
||||
|
||||
let stateFilters = {
|
||||
[PackageStates.AVAILABLE]: false,
|
||||
[PackageStates.NEEDS_UPDATE]: false,
|
||||
[PackageStates.INSTALLED]: false,
|
||||
}
|
||||
|
||||
let sortBy: "popularity" | "most recent" = "popularity";
|
||||
let sortDirection: "asc" | "desc" = "desc";
|
||||
|
||||
const { packages } = packagesStore;
|
||||
$: needsUpdateCount = $packages.filter((p) => p.state === PackageStates.NEEDS_UPDATE).length;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<section class="mt-8 mb-8">
|
||||
<Packages title={$t("home.discover-title")}/>
|
||||
</section>
|
||||
<section class="mt-8 mb-8">
|
||||
<Resources/>
|
||||
</section>
|
||||
|
||||
|
||||
<div id="package-container">
|
||||
<Packages stateFilters={{
|
||||
...stateFilters,
|
||||
[PackageStates.NEEDS_UPDATE]: needsUpdateCount ? stateFilters[PackageStates.NEEDS_UPDATE] : false,
|
||||
}} {sortBy} {sortDirection}/>
|
||||
</div>
|
||||
<aside class={`border border-gray p-2 ${$notificationStore.length ? "lower": ""}`}>
|
||||
<h2 class="text-xl text-primary">Search OSS</h2>
|
||||
<h3 class="text-lg text-primary">Status</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<Checkbox label={"Not installed"} bind:checked={stateFilters[PackageStates.AVAILABLE]} />
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox label={"Installed"} bind:checked={stateFilters[PackageStates.INSTALLED]} />
|
||||
</li>
|
||||
{#if needsUpdateCount}
|
||||
<li>
|
||||
<Checkbox label={`Update Available [${needsUpdateCount}]`} bind:checked={stateFilters[PackageStates.NEEDS_UPDATE]} />
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</aside>
|
||||
<header class={`transition-all px-2 flex justify-between items-center align-middle ${$sideNavOpen ? "min": ""} ${$notificationStore.length ? "lower": ""}`}>
|
||||
<h1 class="text-primary mt-4 text-2xl font-bold">{$t("home.all-packages")}</h1>
|
||||
<section class="border-gray mt-4 mr-4 h-10 w-48 border rounded-sm">
|
||||
<SortingButtons onSort={(prop, dir) => {
|
||||
sortBy = prop;
|
||||
sortDirection = dir;
|
||||
}} />
|
||||
</section>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
#package-container {
|
||||
padding-top: 36px;
|
||||
width: calc(100% - 200px);
|
||||
margin-left: 200px;
|
||||
}
|
||||
|
||||
aside {
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
left: 10px;
|
||||
height: calc(100% - 110px);
|
||||
width: 190px;
|
||||
}
|
||||
aside.lower {
|
||||
top: 140px;
|
||||
}
|
||||
header {
|
||||
position: fixed;
|
||||
top: 40px;
|
||||
left: 0px;
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
background-image: linear-gradient(black, rgba(0,0,0,0.6), rgba(0,0,0,0));
|
||||
}
|
||||
|
||||
header.lower {
|
||||
top: 80px;
|
||||
}
|
||||
|
||||
header.min {
|
||||
width: 75%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
export let active = false;
|
||||
|
||||
export let type: "ghost" | "plain" = "ghost";
|
||||
export let color: "primary" | "secondary" = "primary";
|
||||
export let color: "primary" | "secondary" | "green" = "primary";
|
||||
|
||||
export let loading = false;
|
||||
</script>
|
||||
|
@ -30,13 +30,18 @@
|
|||
}
|
||||
|
||||
button.plain.primary {
|
||||
color: black;
|
||||
background-color: #00ffd0;
|
||||
color: black;
|
||||
}
|
||||
|
||||
button.plain.secondary {
|
||||
color: white;
|
||||
background-color: #8000ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button.plain.green {
|
||||
background-color: #00a517;
|
||||
color: white;
|
||||
}
|
||||
button.active {
|
||||
color: black;
|
||||
|
@ -52,4 +57,7 @@
|
|||
button.secondary:hover {
|
||||
background-color: #8000ff;
|
||||
}
|
||||
button.green:hover {
|
||||
background-color: #00a517;
|
||||
}
|
||||
</style>
|
||||
|
|
81
modules/ui/src/checkbox/checkbox.svelte
Normal file
81
modules/ui/src/checkbox/checkbox.svelte
Normal file
|
@ -0,0 +1,81 @@
|
|||
<script lang="ts">
|
||||
export let label: string;
|
||||
export let checked: boolean;
|
||||
</script>
|
||||
|
||||
<label class="flex justify-between text-sm">
|
||||
<div>{label}</div>
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" bind:checked />
|
||||
<span class="checkmark" />
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<style>
|
||||
label {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Hide the browser's default checkbox */
|
||||
label .checkbox {
|
||||
position: relative;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
label input {
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
/* Create a custom checkbox */
|
||||
.checkmark {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 0;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
/* On mouse-over, add a grey background color */
|
||||
label:hover input ~ .checkmark {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
/* When the checkbox is checked, add a blue background */
|
||||
label input:checked ~ .checkmark {
|
||||
background-color: #00ffd0;
|
||||
}
|
||||
|
||||
/* Create the checkmark/indicator (hidden when not checked) */
|
||||
.checkmark:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show the checkmark when checked */
|
||||
label input:checked ~ .checkmark:after {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Style the checkmark/indicator */
|
||||
label .checkmark:after {
|
||||
left: 5px;
|
||||
top: 2px;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
border: solid white;
|
||||
border-width: 0 3px 3px 0;
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
</style>
|
|
@ -10,37 +10,38 @@
|
|||
export let ctaLabel: string;
|
||||
export let progessLoading = 0;
|
||||
export let ctaType: "ghost" | "plain" = "ghost";
|
||||
export let ctaColor: "green" | "secondary" = "secondary";
|
||||
|
||||
export let onClickCTA = () => {
|
||||
console.log("do nothing");
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="package-card relative h-auto border border-gray p-4">
|
||||
<section class="package-card relative h-auto border border-gray">
|
||||
<a href={link}>
|
||||
<figure class="relative">
|
||||
<ImgLoader
|
||||
class="pkg-image"
|
||||
class="pkg-image object-cover font-sono"
|
||||
src={!pkg.thumb_image_url.includes("https://tea.xyz")
|
||||
? "https://tea.xyz/Images/package-thumb-nolabel4.jpg"
|
||||
: pkg.thumb_image_url}
|
||||
alt={pkg.name}
|
||||
/>
|
||||
<article class="card-thumb-label">
|
||||
<i class="icon-tea-logo-iconasset-1">
|
||||
<!-- TODO: replace with icon.svg -->
|
||||
</i>
|
||||
<h3>{pkg.name}</h3>
|
||||
{#if pkg.maintainer}
|
||||
<h4>• {pkg.maintainer}</h4>
|
||||
{/if}
|
||||
</article>
|
||||
</figure>
|
||||
<article class="card-thumb-label">
|
||||
<h3 class="text-bold text-xl font-bold text-white">{pkg.name}</h3>
|
||||
{#if pkg.maintainer}
|
||||
<h4 class="text-sm font-light">• {pkg.maintainer}</h4>
|
||||
{/if}
|
||||
{#if pkg.desc}
|
||||
<p class="text-xs font-thin line-clamp-2">{pkg.desc}</p>
|
||||
{/if}
|
||||
</article>
|
||||
</a>
|
||||
<footer class="mt-4 flex items-stretch justify-between gap-2">
|
||||
<footer class="absolute bottom-0 left-0 flex w-full items-stretch justify-between gap-2 p-2">
|
||||
<div>
|
||||
<p>
|
||||
<span class="pk-version text-xs">v{pkg.version}</span>
|
||||
<span class="pk-version text-xs font-extralight">v{pkg.version}</span>
|
||||
<!--
|
||||
TODO: uncomment once install counts improve
|
||||
<br>
|
||||
|
@ -48,10 +49,10 @@
|
|||
</p>
|
||||
</div>
|
||||
<Button
|
||||
class="w-1/2 border border-gray p-2 font-machina"
|
||||
class="h-8 w-1/2 border border-gray p-2 font-machina text-xs"
|
||||
onClick={onClickCTA}
|
||||
type={ctaType}
|
||||
color="secondary"
|
||||
color={ctaColor}
|
||||
>
|
||||
{ctaLabel}
|
||||
</Button>
|
||||
|
@ -70,11 +71,12 @@
|
|||
background-color: #1a1a1a;
|
||||
transition: all 0.3s;
|
||||
width: 100%;
|
||||
height: 230px;
|
||||
}
|
||||
|
||||
figure {
|
||||
position: relative;
|
||||
min-height: 150px;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.package-card :global(.pkg-image) {
|
||||
|
@ -82,50 +84,16 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.card-thumb-label i {
|
||||
font-size: 1.5vw;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.card-thumb-label h3 {
|
||||
color: black;
|
||||
font-size: 1.8vw;
|
||||
line-height: 1.8vw;
|
||||
margin: 0px 0px 0.5vw 0vw;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.card-thumb-label {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
left: 0;
|
||||
bottom: 0vw;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
padding: 1.116vw;
|
||||
text-align: left;
|
||||
width: 90%;
|
||||
height: 40%;
|
||||
width: 100%;
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.card-thumb-label h4 {
|
||||
color: black;
|
||||
font-size: 0.9vw;
|
||||
line-height: 1vw;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
.card-thumb-label p {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* button {
|
||||
background-color: #1a1a1a;
|
||||
border: 0.5px solid #ffffff;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
min-width: 120px;
|
||||
transition: 0.1s linear;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #8000ff;
|
||||
box-shadow: inset 0vw 0vw 0vw 0.223vw #1a1a1a !important;
|
||||
} */
|
||||
</style>
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
const green = "#00ffd0";
|
||||
const teal = "#00ffd0";
|
||||
const black = "#1a1a1a";
|
||||
const white = "#fff";
|
||||
const gray = "#949494";
|
||||
const purple = "#8000FF";
|
||||
const green = "#00A517";
|
||||
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||
theme: {
|
||||
colors: {
|
||||
primary: green,
|
||||
primary: teal,
|
||||
secondary: purple,
|
||||
accent: purple,
|
||||
green,
|
||||
teal,
|
||||
purple: {
|
||||
700: purple,
|
||||
900: "#B076EC"
|
||||
|
|
Loading…
Reference in a new issue