mirror of
https://github.com/ivabus/gui
synced 2025-06-08 00:00:27 +03:00
Merge pull request #59 from teaxyz/install-package-on-click
#55 install state and install on click
This commit is contained in:
commit
a61778de55
10 changed files with 241 additions and 90 deletions
22
packages/gui/src-tauri/Cargo.lock
generated
22
packages/gui/src-tauri/Cargo.lock
generated
|
@ -1622,6 +1622,16 @@ dependencies = [
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "os_pipe"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c6a252f1f8c11e84b3ab59d7a488e48e4478a93937e027076638c49536204639"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.42.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "overload"
|
name = "overload"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
@ -2339,6 +2349,16 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shared_child"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "0.3.10"
|
version = "0.3.10"
|
||||||
|
@ -2552,6 +2572,7 @@ dependencies = [
|
||||||
"objc",
|
"objc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"open",
|
"open",
|
||||||
|
"os_pipe",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
|
@ -2561,6 +2582,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
"serialize-to-javascript",
|
"serialize-to-javascript",
|
||||||
|
"shared_child",
|
||||||
"state",
|
"state",
|
||||||
"tar",
|
"tar",
|
||||||
"tauri-macros",
|
"tauri-macros",
|
||||||
|
|
|
@ -17,7 +17,7 @@ tauri-build = { version = "1.2.0", features = [] }
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tauri = { version = "1.2.0", features = ["http-all", "shell-open", "window-all"] }
|
tauri = { version = "1.2.0", features = ["fs-read-dir", "http-all", "shell-all", "window-all"] }
|
||||||
uuid = "1.2.1"
|
uuid = "1.2.1"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn install_package(package: String) {
|
pub fn install_package(package: String) {
|
||||||
println!("installing: {}", package);
|
println!("installing: {}", package);
|
||||||
|
|
||||||
}
|
}
|
|
@ -18,10 +18,26 @@
|
||||||
"scope": ["https://api.tea.xyz/v1/*", "https://github.com/*"]
|
"scope": ["https://api.tea.xyz/v1/*", "https://github.com/*"]
|
||||||
},
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
"all": false,
|
"all": true,
|
||||||
"execute": false,
|
"execute": true,
|
||||||
"open": true,
|
"open": true,
|
||||||
"scope": [],
|
"scope": [
|
||||||
|
{
|
||||||
|
"name": "tea-install",
|
||||||
|
"cmd": "tea",
|
||||||
|
"args": [{ "validator": "\\S+" }, "true"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "node",
|
||||||
|
"cmd": "node",
|
||||||
|
"args": ["--version"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "list-packages",
|
||||||
|
"cmd": "ls",
|
||||||
|
"args": ["-R ~/.tea/tea.xyz/var/www | grep 'xz\\|gz'"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"sidecar": false
|
"sidecar": false
|
||||||
},
|
},
|
||||||
"window": {
|
"window": {
|
||||||
|
@ -55,6 +71,13 @@
|
||||||
"startDragging": true,
|
"startDragging": true,
|
||||||
"unmaximize": true,
|
"unmaximize": true,
|
||||||
"unminimize": true
|
"unminimize": true
|
||||||
|
},
|
||||||
|
"fs": {
|
||||||
|
"readDir": true,
|
||||||
|
"scope": [
|
||||||
|
"$HOME/.tea/*",
|
||||||
|
"$APPDATA/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
|
|
@ -3,42 +3,46 @@
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import { packages as packagesStore, initializePackages } from '$libs/stores';
|
import { packages as packagesStore, initializePackages } from '$libs/stores';
|
||||||
import SortingButtons from './SortingButtons.svelte';
|
import SortingButtons from './SortingButtons.svelte';
|
||||||
import type { Package } from '@tea/ui/types';
|
import type { GUIPackage } from '$libs/types';
|
||||||
|
import { PackageStates } from '$libs/types';
|
||||||
import PackageCard from '@tea/ui/PackageCard/PackageCard.svelte';
|
import PackageCard from '@tea/ui/PackageCard/PackageCard.svelte';
|
||||||
import SearchInput from '@tea/ui/SearchInput/SearchInput.svelte';
|
import SearchInput from '@tea/ui/SearchInput/SearchInput.svelte';
|
||||||
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
|
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
let allPackages: Package[] = [];
|
import { installPackage } from '@api';
|
||||||
let packagesIndex: Fuse<Package>;
|
|
||||||
let packages: Package[] = [];
|
let allPackages: GUIPackage[] = [];
|
||||||
|
let packagesIndex: Fuse<GUIPackage>;
|
||||||
|
let packages: GUIPackage[] = [];
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
let sortBy = 'popularity';
|
let sortBy = 'popularity';
|
||||||
let sortDirection: 'asc' | 'desc' = 'desc';
|
let sortDirection: 'asc' | 'desc' = 'desc';
|
||||||
|
|
||||||
const searchLimit = 5;
|
const searchLimit = 10;
|
||||||
|
|
||||||
const setPackages = (pkgs: Package[]) => {
|
const setPackages = (pkgs: GUIPackage[], isSearch?: boolean) => {
|
||||||
console.log('pkgs sub', pkgs);
|
packages = isSearch
|
||||||
packages = pkgs.sort((a, b) => {
|
? pkgs
|
||||||
if (sortBy === 'popularity') {
|
: pkgs.sort((a, b) => {
|
||||||
const aPop = +a.dl_count + a.installs;
|
if (sortBy === 'popularity') {
|
||||||
const bPop = +b.dl_count + b.installs;
|
const aPop = +a.dl_count + a.installs;
|
||||||
return sortDirection === 'asc' ? aPop - bPop : bPop - aPop;
|
const bPop = +b.dl_count + b.installs;
|
||||||
} else {
|
return sortDirection === 'asc' ? aPop - bPop : bPop - aPop;
|
||||||
// most recent
|
} else {
|
||||||
const aDate = new Date(a.last_modified);
|
// most recent
|
||||||
const bDate = new Date(b.last_modified);
|
const aDate = new Date(a.last_modified);
|
||||||
return sortDirection === 'asc' ? +aDate - +bDate : +bDate - +aDate;
|
const bDate = new Date(b.last_modified);
|
||||||
}
|
return sortDirection === 'asc' ? +aDate - +bDate : +bDate - +aDate;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
packagesStore.subscribe((v) => {
|
packagesStore.subscribe((v) => {
|
||||||
allPackages = v;
|
allPackages = v;
|
||||||
setPackages(allPackages);
|
setPackages(allPackages);
|
||||||
if (!packagesIndex) {
|
if (!packagesIndex && allPackages.length) {
|
||||||
// dont remove or this can get crazy
|
// dont remove or this can get crazy
|
||||||
packagesIndex = new Fuse(allPackages, {
|
packagesIndex = new Fuse(allPackages, {
|
||||||
keys: ['name', 'full_name', 'desc']
|
keys: ['name', 'full_name', 'desc']
|
||||||
|
@ -54,15 +58,11 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSearch = (term: string) => {
|
const onSearch = (term: string) => {
|
||||||
if (term !== '' && term.length > 3) {
|
if (term !== '' && term.length > 1) {
|
||||||
const res = packagesIndex.search(term);
|
const res = packagesIndex.search(term, { limit: searchLimit });
|
||||||
const matchingPackages = [];
|
const matchingPackages: GUIPackage[] = res.map((v) => v.item);
|
||||||
for (let i = 0; i < searchLimit; i++) {
|
|
||||||
if (res[i]) {
|
setPackages(matchingPackages, true);
|
||||||
matchingPackages.push(res[i].item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setPackages(matchingPackages);
|
|
||||||
} else {
|
} else {
|
||||||
setPackages(allPackages);
|
setPackages(allPackages);
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,15 @@
|
||||||
sortDirection = dir;
|
sortDirection = dir;
|
||||||
setPackages(packages);
|
setPackages(packages);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCTALabel = (state: PackageStates): string => {
|
||||||
|
return {
|
||||||
|
[PackageStates.AVAILABLE]: 'INSTALL',
|
||||||
|
[PackageStates.INSTALLED]: 'INSTALLED',
|
||||||
|
[PackageStates.INSTALLING]: 'INSTALLING',
|
||||||
|
[PackageStates.UNINSTALLED]: 'RE-INSTALL'
|
||||||
|
}[state];
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="border border-gray bg-black">
|
<div class="border border-gray bg-black">
|
||||||
|
@ -87,9 +96,24 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<ul class="grid grid-cols-3">
|
<ul class="grid grid-cols-3">
|
||||||
{#if packages.length}
|
{#if packages.length > 0}
|
||||||
{#each packages as pkg}
|
{#each packages as pkg}
|
||||||
<PackageCard {pkg} link={`/packages/${pkg.slug}`} />
|
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
|
||||||
|
<PackageCard
|
||||||
|
{pkg}
|
||||||
|
link={`/packages/${pkg.slug}`}
|
||||||
|
ctaLabel={getCTALabel(pkg.state)}
|
||||||
|
onClickCTA={async () => {
|
||||||
|
try {
|
||||||
|
pkg.state = PackageStates.INSTALLING;
|
||||||
|
await installPackage(pkg.full_name);
|
||||||
|
pkg.state = PackageStates.INSTALLED;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
{#each Array(12) as _}
|
{#each Array(12) as _}
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
* * make cors work with api.tea.xyz/v1
|
* * make cors work with api.tea.xyz/v1
|
||||||
*/
|
*/
|
||||||
import type { Package, Review } from '@tea/ui/types';
|
import type { Package, Review } from '@tea/ui/types';
|
||||||
|
import type { GUIPackage } from '../types';
|
||||||
|
import { PackageStates } from '../types';
|
||||||
import { loremIpsum } from 'lorem-ipsum';
|
import { loremIpsum } from 'lorem-ipsum';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
@ -152,9 +154,14 @@ const packages: Package[] = [
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function getPackages(): Promise<Package[]> {
|
export async function getPackages(): Promise<GUIPackage[]> {
|
||||||
await delay(2000);
|
await delay(2000);
|
||||||
return packages;
|
return packages.map((pkg) => {
|
||||||
|
return {
|
||||||
|
...pkg,
|
||||||
|
state: PackageStates.AVAILABLE
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFeaturedPackages(): Promise<Package[]> {
|
export async function getFeaturedPackages(): Promise<Package[]> {
|
||||||
|
@ -201,6 +208,11 @@ export async function getPackageReviews(full_name: string): Promise<Review[]> {
|
||||||
return reviews;
|
return reviews;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function installPackage(full_name: string) {
|
||||||
|
console.log('installing: ', full_name);
|
||||||
|
await delay(10000);
|
||||||
|
}
|
||||||
|
|
||||||
function delay(ms: number) {
|
function delay(ms: number) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,14 @@
|
||||||
* - connect to a local platform api and returns a data
|
* - connect to a local platform api and returns a data
|
||||||
*/
|
*/
|
||||||
import { getClient } from '@tauri-apps/api/http';
|
import { getClient } from '@tauri-apps/api/http';
|
||||||
|
// import { invoke } from '@tauri-apps/api';
|
||||||
|
import { Command } from '@tauri-apps/api/shell';
|
||||||
|
import { readDir, BaseDirectory } from '@tauri-apps/api/fs';
|
||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
import type { Package, Review } from '../types';
|
import type { Package, Review } from '@tea/ui/types';
|
||||||
|
import type { GUIPackage } from '../types';
|
||||||
import * as mock from './mock';
|
import * as mock from './mock';
|
||||||
|
import { PackageStates } from '../types';
|
||||||
|
|
||||||
const username = 'user';
|
const username = 'user';
|
||||||
const password = 'password';
|
const password = 'password';
|
||||||
|
@ -46,9 +51,20 @@ const join = function (...paths: string[]) {
|
||||||
.join('/');
|
.join('/');
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getPackages(): Promise<Package[]> {
|
export async function getPackages(): Promise<GUIPackage[]> {
|
||||||
const packages = await get<Package[]>('packages');
|
const [packages, installedPackages] = await Promise.all([
|
||||||
return packages;
|
get<Package[]>('packages'),
|
||||||
|
getInstalledPackages()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return packages.map((pkg) => {
|
||||||
|
const found = installedPackages.find((p) => p.full_name === pkg.full_name);
|
||||||
|
return {
|
||||||
|
...pkg,
|
||||||
|
state: found ? PackageStates.INSTALLED : PackageStates.AVAILABLE,
|
||||||
|
installed_version: found ? found.version : ''
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFeaturedPackages(): Promise<Package[]> {
|
export async function getFeaturedPackages(): Promise<Package[]> {
|
||||||
|
@ -62,3 +78,59 @@ export async function getPackageReviews(full_name: string): Promise<Review[]> {
|
||||||
|
|
||||||
return reviews;
|
return reviews;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function installPackage(full_name: string) {
|
||||||
|
try {
|
||||||
|
await installPackageCommand(full_name);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installPackageCommand(full_name: string) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const teaInstallCommand = new Command('tea-install', [`+${full_name}`, 'true']);
|
||||||
|
teaInstallCommand.on('error', reject);
|
||||||
|
|
||||||
|
const handleLineOutput = async (line: string | any) => {
|
||||||
|
const c = await child;
|
||||||
|
if (line?.code === 0 || line.includes('installed:')) {
|
||||||
|
c.kill();
|
||||||
|
resolve(c.pid);
|
||||||
|
} else if (line?.code === 1) {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
teaInstallCommand.stdout.on('data', handleLineOutput);
|
||||||
|
teaInstallCommand.stderr.on('data', handleLineOutput);
|
||||||
|
teaInstallCommand.on('close', (line: string) => {
|
||||||
|
console.log('command closed!');
|
||||||
|
handleLineOutput(line || '');
|
||||||
|
});
|
||||||
|
teaInstallCommand.on('error', (line: string) => {
|
||||||
|
console.log('command error!', line);
|
||||||
|
handleLineOutput(line || '');
|
||||||
|
});
|
||||||
|
const child = teaInstallCommand.spawn();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getInstalledPackages() {
|
||||||
|
const entries = await readDir('.tea/tea.xyz/var/www', {
|
||||||
|
dir: BaseDirectory.Home,
|
||||||
|
recursive: false
|
||||||
|
});
|
||||||
|
const packages = entries
|
||||||
|
.filter((o) => o.path.match('^(.*).(g|x)z$'))
|
||||||
|
.map((o) => {
|
||||||
|
const [pkg_version] = (o?.name || '').split('+');
|
||||||
|
const version = pkg_version.split('-').pop();
|
||||||
|
const full_name = pkg_version.replace(`-${version}`, '');
|
||||||
|
return {
|
||||||
|
full_name,
|
||||||
|
version
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return packages;
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import type { Package, Review } from '@tea/ui/types';
|
import type { Package, Review } from '@tea/ui/types';
|
||||||
|
import type { GUIPackage } from '$libs/types';
|
||||||
// TODO: figure out a better structure for managing states maybe turn them into models?
|
// TODO: figure out a better structure for managing states maybe turn them into models?
|
||||||
|
|
||||||
import { getPackages, getFeaturedPackages, getPackageReviews } from '@api';
|
import { getPackages, getFeaturedPackages, getPackageReviews } from '@api';
|
||||||
|
|
||||||
export const backLink = writable<string>('/');
|
export const backLink = writable<string>('/');
|
||||||
|
|
||||||
export const packages = writable<Package[]>([]);
|
export const packages = writable<GUIPackage[]>([]);
|
||||||
|
|
||||||
export const featuredPackages = writable<Package[]>([]);
|
export const featuredPackages = writable<Package[]>([]);
|
||||||
|
|
||||||
|
|
|
@ -2,3 +2,17 @@
|
||||||
// else
|
// else
|
||||||
// please use the package @tea/ui/src/types.ts
|
// please use the package @tea/ui/src/types.ts
|
||||||
// things that go there are shared types/shapes like ie: Package
|
// things that go there are shared types/shapes like ie: Package
|
||||||
|
|
||||||
|
import type { Package } from '@tea/ui/types';
|
||||||
|
|
||||||
|
export enum PackageStates {
|
||||||
|
AVAILABLE,
|
||||||
|
INSTALLED,
|
||||||
|
INSTALLING,
|
||||||
|
UNINSTALLED
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GUIPackage = Package & {
|
||||||
|
state: PackageStates;
|
||||||
|
installed_version?: string;
|
||||||
|
};
|
||||||
|
|
|
@ -2,29 +2,37 @@
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
import type { Package } from '../types';
|
import type { Package } from '../types';
|
||||||
import ImgLoader from '../ImgLoader/ImgLoader.svelte';
|
import ImgLoader from '../ImgLoader/ImgLoader.svelte';
|
||||||
|
|
||||||
export let pkg: Package;
|
export let pkg: Package;
|
||||||
export let link: string;
|
export let link: string;
|
||||||
|
export let ctaLabel: string;
|
||||||
|
|
||||||
|
export let onClickCTA = () => {
|
||||||
|
console.log('do nothing');
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="package-card border border-gray p-4">
|
<section class="package-card border border-gray p-4">
|
||||||
<figure>
|
<a href={link}>
|
||||||
<ImgLoader
|
<figure>
|
||||||
class="pkg-image"
|
<ImgLoader
|
||||||
src={!pkg.thumb_image_url.includes('https://tea.xyz')
|
class="pkg-image"
|
||||||
? 'https://tea.xyz/Images/package-thumb-nolabel4.jpg'
|
src={!pkg.thumb_image_url.includes('https://tea.xyz')
|
||||||
: pkg.thumb_image_url}
|
? 'https://tea.xyz/Images/package-thumb-nolabel4.jpg'
|
||||||
alt={pkg.name}
|
: pkg.thumb_image_url}
|
||||||
/>
|
alt={pkg.name}
|
||||||
<article class="card-thumb-label">
|
/>
|
||||||
<i class="icon-tea-logo-iconasset-1">
|
<article class="card-thumb-label">
|
||||||
<!-- TODO: replace with icon.svg -->
|
<i class="icon-tea-logo-iconasset-1">
|
||||||
</i>
|
<!-- TODO: replace with icon.svg -->
|
||||||
<h3>{pkg.name}</h3>
|
</i>
|
||||||
{#if pkg.maintainer}
|
<h3>{pkg.name}</h3>
|
||||||
<h4>• {pkg.maintainer}</h4>
|
{#if pkg.maintainer}
|
||||||
{/if}
|
<h4>• {pkg.maintainer}</h4>
|
||||||
</article>
|
{/if}
|
||||||
</figure>
|
</article>
|
||||||
|
</figure>
|
||||||
|
</a>
|
||||||
<footer class="mt-4 flex items-center justify-between">
|
<footer class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
|
@ -35,10 +43,7 @@
|
||||||
<span class="package-install-no">>{{- .installs -}} installs</span> -->
|
<span class="package-install-no">>{{- .installs -}} installs</span> -->
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- TODO: move this button into its own reusable component -->
|
<button class="p-2 font-machina" on:click={onClickCTA}>{ctaLabel}</button>
|
||||||
<a href={link}>
|
|
||||||
<button class="detail-btn"><i class="icon-enter-arrow" />details</button>
|
|
||||||
</a>
|
|
||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -91,40 +96,18 @@
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-btn {
|
button {
|
||||||
position: relative;
|
|
||||||
float: right;
|
|
||||||
right: 0;
|
|
||||||
display: inline-block;
|
|
||||||
font-family: 'pp-neue-machina', sans-serif;
|
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
border: 0.5px solid #ffffff;
|
border: 0.5px solid #ffffff;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding-top: 0.279vw;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
width: 120px;
|
min-width: 120px;
|
||||||
height: 2.232vw;
|
|
||||||
min-height: 34px;
|
|
||||||
transition: 0.1s linear;
|
transition: 0.1s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-btn:hover {
|
button:hover {
|
||||||
background-color: #8000ff;
|
background-color: #8000ff;
|
||||||
box-shadow: inset 0vw 0vw 0vw 0.223vw #1a1a1a !important;
|
box-shadow: inset 0vw 0vw 0vw 0.223vw #1a1a1a !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icon Styling */
|
|
||||||
|
|
||||||
.detail-btn .icon-enter-arrow {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
margin-right: 0.558vw;
|
|
||||||
transition: 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-btn:hover .icon-enter-arrow {
|
|
||||||
display: inline-block;
|
|
||||||
transform: rotate(-45deg) !important;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in a new issue