Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
704a3d0417 | ||
|
|
22d6c7991e | ||
|
|
795b7f0321 |
15
.github/actions/upload-artifact/action.yaml
vendored
Normal file
15
.github/actions/upload-artifact/action.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
inputs:
|
||||||
|
name:
|
||||||
|
required: true
|
||||||
|
path:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.name }}
|
||||||
|
path: ${{ inputs.path }}
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Upload screenshot
|
name: Upload screenshot
|
||||||
if: always() # Run even if previous step fails
|
if: always() # Run even if previous step fails
|
||||||
uses: actions/upload-artifact@v3
|
uses: ./.github/actions/upload-artifact
|
||||||
with:
|
with:
|
||||||
name: screenshot-${{ matrix.os }}
|
name: screenshot-${{ matrix.os }}
|
||||||
path: screenshot.png
|
path: screenshot.png
|
||||||
|
|||||||
4
.github/workflows/tests.e2e.yaml
vendored
4
.github/workflows/tests.e2e.yaml
vendored
@@ -51,14 +51,14 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Upload screenshots
|
name: Upload screenshots
|
||||||
if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed
|
if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed
|
||||||
uses: actions/upload-artifact@v3
|
uses: ./.github/actions/upload-artifact
|
||||||
with:
|
with:
|
||||||
name: e2e-screenshots-${{ matrix.os }}
|
name: e2e-screenshots-${{ matrix.os }}
|
||||||
path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }}
|
path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }}
|
||||||
-
|
-
|
||||||
name: Upload videos
|
name: Upload videos
|
||||||
if: always() # Run even if previous steps fail because test run video is always captured
|
if: always() # Run even if previous steps fail because test run video is always captured
|
||||||
uses: actions/upload-artifact@v3
|
uses: ./.github/actions/upload-artifact
|
||||||
with:
|
with:
|
||||||
name: e2e-videos-${{ matrix.os }}
|
name: e2e-videos-${{ matrix.os }}
|
||||||
path: ${{ steps.artifacts.outputs.VIDEOS_DIR }}
|
path: ${{ steps.artifacts.outputs.VIDEOS_DIR }}
|
||||||
|
|||||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,5 +1,27 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.13.4 (2024-05-27)
|
||||||
|
|
||||||
|
* Add specific empty function name compiler error | [870120b](https://github.com/undergroundwires/privacy.sexy/commit/870120bc13909a3681e0f0a2351806849476342f)
|
||||||
|
* ci/cd: fix recent Docker build failures on macOS | [a1922c5](https://github.com/undergroundwires/privacy.sexy/commit/a1922c50c12b3b7806e9e681ace842194a178bda)
|
||||||
|
* win: standardize registry edit + delete on revert | [cec0b4b](https://github.com/undergroundwires/privacy.sexy/commit/cec0b4b4f63c3563a0e7923ce6324a38d71a3955)
|
||||||
|
* Fix e2e test failing on Windows | [4a7efa2](https://github.com/undergroundwires/privacy.sexy/commit/4a7efa27c8df73ef9b7960afed29f216b066cba2)
|
||||||
|
* Add support for macOS universal binary #348, #362 | [d25c4e8](https://github.com/undergroundwires/privacy.sexy/commit/d25c4e8c812b8d012010ba38070a2931dcd28908)
|
||||||
|
* Migrate to GitHub issue forms | [9ab3ff7](https://github.com/undergroundwires/privacy.sexy/commit/9ab3ff75b0a69ac2ba27dd02e82db9b5bd76ea0f)
|
||||||
|
* ci/cd: fix quality checks not running on all OSes | [2390530](https://github.com/undergroundwires/privacy.sexy/commit/2390530d929fb92c266558c52376569a0ecb90c1)
|
||||||
|
* Bump Vue to latest and fix universal selector CSS | [aae5434](https://github.com/undergroundwires/privacy.sexy/commit/aae54344511ec51d17ad0420a92cb5a064e0e7bb)
|
||||||
|
* Centralize and optimize `ResizeObserver` usage | [2923621](https://github.com/undergroundwires/privacy.sexy/commit/292362135db0519ec1050bab80ed373aad115731)
|
||||||
|
* win: improve app access disabling and docs #138 | [ff3d5c4](https://github.com/undergroundwires/privacy.sexy/commit/ff3d5c48419f663379f5aba8936636c22f2c5de8)
|
||||||
|
* win: document and discourage RSA key script #363 | [f347fde](https://github.com/undergroundwires/privacy.sexy/commit/f347fde0c85f8b51b0060fdea0a2724b042aaeed)
|
||||||
|
* win: improve printing removal /w Print Queue #279 | [150e067](https://github.com/undergroundwires/privacy.sexy/commit/150e0670392bb62348c20ec644a4ed8a6bbffe74)
|
||||||
|
* win: discourage blocking app access #121 #339 #350 | [7794846](https://github.com/undergroundwires/privacy.sexy/commit/77948461856e6837ddfbcbbef72a1bf9fc706b4e)
|
||||||
|
* Improve context for errors thrown by compiler | [4212c7b](https://github.com/undergroundwires/privacy.sexy/commit/4212c7b9e0b1500378a1e4e88efc2d59f39f3d29)
|
||||||
|
* win: document disabling firewall #115 #152 #364 | [12b1f18](https://github.com/undergroundwires/privacy.sexy/commit/12b1f183f7ce966d6ce090d98aeea7ec491f8c7c)
|
||||||
|
* win: add script to disable Recall feature | [ce4cfdd](https://github.com/undergroundwires/privacy.sexy/commit/ce4cfdd169b7da0edc3da61143c988ed5f3c976e)
|
||||||
|
* win, mac, linux: fix typos and dead URLs #367 | [9e34e64](https://github.com/undergroundwires/privacy.sexy/commit/9e34e644493674ca709b64a47206763d5d4bd60c)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.3...0.13.4)
|
||||||
|
|
||||||
## 0.13.3 (2024-05-11)
|
## 0.13.3 (2024-05-11)
|
||||||
|
|
||||||
* win: organize and document network disablement | [2eed6f4](https://github.com/undergroundwires/privacy.sexy/commit/2eed6f4afb6cf85fdc1d6acb808f82405a35cafd)
|
* win: organize and document network disablement | [2eed6f4](https://github.com/undergroundwires/privacy.sexy/commit/2eed6f4afb6cf85fdc1d6acb808f82405a35cafd)
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||||
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.3/privacy.sexy-Setup-0.13.3.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.3/privacy.sexy-0.13.3.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.3/privacy.sexy-0.13.3.AppImage). For more options, see [here](#additional-install-options).
|
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.4/privacy.sexy-Setup-0.13.4.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.4/privacy.sexy-0.13.4.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.4/privacy.sexy-0.13.4.AppImage). For more options, see [here](#additional-install-options).
|
||||||
|
|
||||||
See also:
|
See also:
|
||||||
|
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.13.3",
|
"version": "0.13.4",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.13.3",
|
"version": "0.13.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"slogan": "Privacy is sexy",
|
"slogan": "Privacy is sexy",
|
||||||
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy.",
|
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy.",
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<CardExpansionArrow />
|
||||||
|
<div class="card__expander">
|
||||||
|
<div class="card__expander__close-button">
|
||||||
|
<FlatButton
|
||||||
|
icon="xmark"
|
||||||
|
@click="collapse()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="card__expander__content">
|
||||||
|
<ScriptsTree
|
||||||
|
:category-id="categoryId"
|
||||||
|
:has-top-padding="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
defineComponent,
|
||||||
|
} from 'vue';
|
||||||
|
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
|
||||||
|
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
|
||||||
|
import CardExpansionArrow from './CardExpansionArrow.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
ScriptsTree,
|
||||||
|
FlatButton,
|
||||||
|
CardExpansionArrow,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
categoryId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
onCollapse: () => true,
|
||||||
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
|
},
|
||||||
|
setup(_, { emit }) {
|
||||||
|
function collapse() {
|
||||||
|
emit('onCollapse');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
collapse,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "@/presentation/assets/styles/main" as *;
|
||||||
|
@use "./card-gap" as *;
|
||||||
|
|
||||||
|
.card__expander {
|
||||||
|
position: relative;
|
||||||
|
background-color: $color-primary-darker;
|
||||||
|
color: $color-on-primary;
|
||||||
|
margin-top: $spacing-absolute-xx-large;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.card__expander__content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
word-break: break-word;
|
||||||
|
max-width: 100%; // Prevents horizontal expansion of inner content (e.g., when a code block is shown)
|
||||||
|
width: 100%; // Expands the container to fill available horizontal space, enabling alignment of child items.
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__expander__close-button {
|
||||||
|
font-size: $font-size-absolute-large;
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-right: $spacing-absolute-small;
|
||||||
|
@include clickable;
|
||||||
|
color: $color-primary-light;
|
||||||
|
@include hover-or-touch {
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
:data-category="categoryId"
|
:data-category="categoryId"
|
||||||
:category-id="categoryId"
|
:category-id="categoryId"
|
||||||
:active-category-id="activeCategoryId"
|
:active-category-id="activeCategoryId"
|
||||||
|
:card-layout="cardLayout"
|
||||||
@card-expansion-changed="onSelected(categoryId, $event)"
|
@card-expansion-changed="onSelected(categoryId, $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,6 +47,7 @@ import { injectKey } from '@/presentation/injectionSymbols';
|
|||||||
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
|
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
|
||||||
import { hasDirective } from './NonCollapsingDirective';
|
import { hasDirective } from './NonCollapsingDirective';
|
||||||
import CardListItem from './CardListItem.vue';
|
import CardListItem from './CardListItem.vue';
|
||||||
|
import { useCardLayout } from './UseCardLayout';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -61,8 +63,14 @@ export default defineComponent({
|
|||||||
const categoryIds = computed<readonly number[]>(
|
const categoryIds = computed<readonly number[]>(
|
||||||
() => currentState.value.collection.actions.map((category) => category.id),
|
() => currentState.value.collection.actions.map((category) => category.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeCategoryId = ref<number | undefined>(undefined);
|
const activeCategoryId = ref<number | undefined>(undefined);
|
||||||
|
|
||||||
|
const cardLayout = useCardLayout({
|
||||||
|
containerWidth: computed(() => width.value ?? 0),
|
||||||
|
totalCards: computed(() => categoryIds.value.length),
|
||||||
|
});
|
||||||
|
|
||||||
function onSelected(categoryId: number, isExpanded: boolean) {
|
function onSelected(categoryId: number, isExpanded: boolean) {
|
||||||
activeCategoryId.value = isExpanded ? categoryId : undefined;
|
activeCategoryId.value = isExpanded ? categoryId : undefined;
|
||||||
}
|
}
|
||||||
@@ -101,6 +109,7 @@ export default defineComponent({
|
|||||||
width,
|
width,
|
||||||
categoryIds,
|
categoryIds,
|
||||||
activeCategoryId,
|
activeCategoryId,
|
||||||
|
cardLayout,
|
||||||
onSelected,
|
onSelected,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,26 +29,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CardExpandTransition>
|
<CardExpandTransition>
|
||||||
<div v-show="isExpanded">
|
<CardExpansionPanel
|
||||||
<CardExpansionArrow />
|
v-show="isExpanded"
|
||||||
<div
|
|
||||||
class="card__expander"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<div class="card__expander__close-button">
|
|
||||||
<FlatButton
|
|
||||||
icon="xmark"
|
|
||||||
@click="collapse()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="card__expander__content">
|
|
||||||
<ScriptsTree
|
|
||||||
:category-id="categoryId"
|
:category-id="categoryId"
|
||||||
:has-top-padding="false"
|
@on-collapse="collapse"
|
||||||
|
@click.stop
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardExpandTransition>
|
</CardExpandTransition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -56,30 +42,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, computed, shallowRef,
|
defineComponent, computed, shallowRef,
|
||||||
|
type PropType,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||||
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
|
|
||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
|
|
||||||
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
|
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
|
||||||
import CardSelectionIndicator from './CardSelectionIndicator.vue';
|
import CardSelectionIndicator from './CardSelectionIndicator.vue';
|
||||||
import CardExpandTransition from './CardExpandTransition.vue';
|
import CardExpandTransition from './CardExpandTransition.vue';
|
||||||
import CardExpansionArrow from './CardExpansionArrow.vue';
|
import CardExpansionPanel from './CardExpansionPanel.vue';
|
||||||
|
import type { CardLayout } from './UseCardLayout';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
ScriptsTree,
|
|
||||||
AppIcon,
|
AppIcon,
|
||||||
CardSelectionIndicator,
|
CardSelectionIndicator,
|
||||||
FlatButton,
|
CardExpansionPanel,
|
||||||
CardExpandTransition,
|
CardExpandTransition,
|
||||||
CardExpansionArrow,
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
categoryId: {
|
categoryId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
cardLayout: {
|
||||||
|
type: Object as PropType<CardLayout>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
activeCategoryId: {
|
activeCategoryId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
@@ -129,6 +117,7 @@ export default defineComponent({
|
|||||||
cardTitle,
|
cardTitle,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
cardElement,
|
cardElement,
|
||||||
|
totalColumns: props.cardLayout.totalColumns,
|
||||||
collapse,
|
collapse,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -141,7 +130,6 @@ export default defineComponent({
|
|||||||
@use "./card-gap" as *;
|
@use "./card-gap" as *;
|
||||||
|
|
||||||
$card-inner-padding : $spacing-absolute-xx-large;
|
$card-inner-padding : $spacing-absolute-xx-large;
|
||||||
$expanded-margin-top : $spacing-absolute-xx-large;
|
|
||||||
$card-horizontal-gap : $card-gap;
|
$card-horizontal-gap : $card-gap;
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
@@ -190,44 +178,13 @@ $card-horizontal-gap : $card-gap;
|
|||||||
font-size: $font-size-absolute-normal;
|
font-size: $font-size-absolute-normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.card__expander {
|
|
||||||
position: relative;
|
|
||||||
background-color: $color-primary-darker;
|
|
||||||
color: $color-on-primary;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.card__expander__content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
word-break: break-word;
|
|
||||||
max-width: 100%; // Prevents horizontal expansion of inner content (e.g., when a code block is shown)
|
|
||||||
width: 100%; // Expands the container to fill available horizontal space, enabling alignment of child items.
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__expander__close-button {
|
|
||||||
font-size: $font-size-absolute-large;
|
|
||||||
align-self: flex-end;
|
|
||||||
margin-right: $spacing-absolute-small;
|
|
||||||
@include clickable;
|
|
||||||
color: $color-primary-light;
|
|
||||||
@include hover-or-touch {
|
|
||||||
color: $color-primary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-expanded {
|
&.is-expanded {
|
||||||
.card__inner {
|
.card__inner {
|
||||||
height: auto;
|
height: auto;
|
||||||
background-color: $color-secondary;
|
background-color: $color-secondary;
|
||||||
color: $color-on-secondary;
|
color: $color-on-secondary;
|
||||||
}
|
margin-bottom: $spacing-absolute-xx-large;
|
||||||
|
|
||||||
.card__expander {
|
|
||||||
margin-top: $expanded-margin-top;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@include hover-or-touch {
|
@include hover-or-touch {
|
||||||
@@ -253,36 +210,32 @@ $card-horizontal-gap : $card-gap;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@mixin adaptive-card($cards-in-row) {
|
|
||||||
&.card {
|
.card {
|
||||||
$total-times-gap-is-used-in-row: $cards-in-row - 1;
|
$total-columns: v-bind(totalColumns);
|
||||||
$total-gap-width-in-row: $total-times-gap-is-used-in-row * $card-horizontal-gap;
|
$total-times-gap-is-used-in-row: calc($total-columns - 1);
|
||||||
|
$total-gap-width-in-row: calc($total-times-gap-is-used-in-row * $card-horizontal-gap);
|
||||||
$available-row-width-for-cards: calc(100% - #{$total-gap-width-in-row});
|
$available-row-width-for-cards: calc(100% - #{$total-gap-width-in-row});
|
||||||
$available-width-per-card: calc(#{$available-row-width-for-cards} / #{$cards-in-row});
|
$available-width-per-card: calc(#{$available-row-width-for-cards} / $total-columns);
|
||||||
width:$available-width-per-card;
|
width:$available-width-per-card;
|
||||||
.card__expander {
|
:deep(.card__expander) {
|
||||||
$all-cards-width: 100% * $cards-in-row;
|
$all-cards-width: calc(100% * $total-columns);
|
||||||
$additional-padding-width: $card-horizontal-gap * ($cards-in-row - 1);
|
$additional-padding-width: calc($card-horizontal-gap * ($total-columns - 1));
|
||||||
width: calc(#{$all-cards-width} + #{$additional-padding-width});
|
width: calc(#{$all-cards-width} + #{$additional-padding-width});
|
||||||
}
|
}
|
||||||
@for $nth-card from 2 through $cards-in-row { // From second card to rest
|
// @for $nth-card from 2 through $total-columns { // From second card to rest
|
||||||
&:nth-of-type(#{$cards-in-row}n+#{$nth-card}) {
|
// &:nth-of-type(#{$total-columns}n+#{$nth-card}) {
|
||||||
.card__expander {
|
// :deep(.card__expander) {
|
||||||
$card-left: -100% * ($nth-card - 1);
|
// $card-left: -100% * ($nth-card - 1);
|
||||||
$additional-space: $card-horizontal-gap * ($nth-card - 1);
|
// $additional-space: $card-horizontal-gap * ($nth-card - 1);
|
||||||
margin-left: calc(#{$card-left} - #{$additional-space});
|
// margin-left: calc(#{$card-left} - #{$additional-space});
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
// Ensure new line after last row
|
// Ensure new line after last row
|
||||||
$card-after-last: $cards-in-row + 1;
|
$card-after-last: $total-columns + 1;
|
||||||
&:nth-of-type(#{$cards-in-row}n+#{$card-after-last}) {
|
&:nth-of-type(#{$total-columns}n+#{$card-after-last}) {
|
||||||
clear: left;
|
clear: left;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.big-screen { @include adaptive-card(3); }
|
|
||||||
.medium-screen { @include adaptive-card(2); }
|
|
||||||
.small-screen { @include adaptive-card(1); }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { computed, type Ref } from 'vue';
|
||||||
|
|
||||||
|
export function useCardLayout(options: {
|
||||||
|
readonly containerWidth: Readonly<Ref<number>>;
|
||||||
|
readonly totalCards: Readonly<Ref<number>>;
|
||||||
|
}): Readonly<Ref<CardLayout>> {
|
||||||
|
return computed(() => {
|
||||||
|
return determineCardLayout(
|
||||||
|
options.containerWidth.value,
|
||||||
|
options.totalCards.value,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardLayout {
|
||||||
|
readonly totalRows: number;
|
||||||
|
readonly totalColumns: number;
|
||||||
|
readonly availableCardWidth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function determineCardLayout(
|
||||||
|
containerWidth: number,
|
||||||
|
totalCards: number,
|
||||||
|
): CardLayout {
|
||||||
|
const containerSize = getContainerSize(containerWidth);
|
||||||
|
const totalColumns = countTotalColumns(containerSize);
|
||||||
|
const totalRows = countTotalRows(totalColumns, totalCards);
|
||||||
|
return {
|
||||||
|
totalColumns,
|
||||||
|
totalRows,
|
||||||
|
availableCardWidth: containerWidth / totalRows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ContainerSize {
|
||||||
|
Small,
|
||||||
|
Medium,
|
||||||
|
Big,
|
||||||
|
}
|
||||||
|
|
||||||
|
function countTotalRows(totalColumns: number, totalCards: number): number {
|
||||||
|
return Math.ceil(totalCards / totalColumns);
|
||||||
|
}
|
||||||
|
|
||||||
|
function countTotalColumns(size: ContainerSize): number {
|
||||||
|
switch (size) {
|
||||||
|
case ContainerSize.Small:
|
||||||
|
return 1;
|
||||||
|
case ContainerSize.Medium:
|
||||||
|
return 2;
|
||||||
|
case ContainerSize.Big:
|
||||||
|
return 3;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown size: ${size}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContainerSize(containerWidth: number): ContainerSize {
|
||||||
|
const smallBreakpoint = 500;
|
||||||
|
const bigBreakpoint = 750;
|
||||||
|
if (containerWidth <= smallBreakpoint) {
|
||||||
|
return ContainerSize.Small;
|
||||||
|
}
|
||||||
|
if (containerWidth < bigBreakpoint) {
|
||||||
|
return ContainerSize.Medium;
|
||||||
|
}
|
||||||
|
return ContainerSize.Big;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user