@newschools/sdk 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/nuxt/src/runtime/components/BentoGrid.vue +38 -0
- package/nuxt/src/runtime/components/BentoGridItem.vue +49 -0
- package/nuxt/src/runtime/components/EntityCard.vue +126 -0
- package/nuxt/src/runtime/components/EntityImage.vue +127 -0
- package/nuxt/src/runtime/components/OrganizationHero.vue +10 -44
- package/nuxt/src/runtime/composables/useOrganization.ts +4 -3
- package/nuxt/src/runtime/internal/FlickeringGridBackground.vue +198 -0
- package/nuxt/src/runtime/internal/icons/BrokenImageIcon.vue +31 -0
- package/nuxt/src/runtime/plugin.ts +11 -5
- package/nuxt/src/types/entities.ts +78 -0
- package/package.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { HTMLAttributes } from "vue";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
class?: HTMLAttributes["class"];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const props = defineProps<Props>();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div class="ns-bento-grid" :class="[props.class]">
|
|
13
|
+
<slot />
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<style lang="scss" scoped>
|
|
18
|
+
/**
|
|
19
|
+
* Bento Grid Component
|
|
20
|
+
* Asymmetric grid layout inspired by Japanese bento boxes
|
|
21
|
+
* Mobile: Single column stack
|
|
22
|
+
* Tablet+: 3-column grid with auto rows
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
.ns-bento-grid {
|
|
26
|
+
/* Mobile first: Single column stack */
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
gap: var(--ns-spacing-md);
|
|
30
|
+
|
|
31
|
+
/* Tablet and up: 3 column bento grid with auto rows */
|
|
32
|
+
@media (min-width: 768px) {
|
|
33
|
+
display: grid;
|
|
34
|
+
grid-template-columns: repeat(3, 1fr);
|
|
35
|
+
grid-auto-rows: 18rem;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
</style>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { HTMLAttributes } from "vue";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
class?: HTMLAttributes["class"];
|
|
6
|
+
/** Number of rows to span (default: 1) */
|
|
7
|
+
rowSpan?: number;
|
|
8
|
+
/** Number of columns to span (default: 1) */
|
|
9
|
+
colSpan?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
13
|
+
rowSpan: 1,
|
|
14
|
+
colSpan: 1,
|
|
15
|
+
});
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div
|
|
20
|
+
class="ns-bento-grid-item"
|
|
21
|
+
:class="[props.class]"
|
|
22
|
+
:style="{
|
|
23
|
+
gridRow: `span ${props.rowSpan}`,
|
|
24
|
+
gridColumn: `span ${props.colSpan}`,
|
|
25
|
+
}"
|
|
26
|
+
>
|
|
27
|
+
<slot />
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<style lang="scss" scoped>
|
|
32
|
+
/**
|
|
33
|
+
* Bento Grid Item Component
|
|
34
|
+
* Individual item within bento grid with span control
|
|
35
|
+
* Converted from Tailwind classes - simplified for use with EntityCard
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
.ns-bento-grid-item {
|
|
39
|
+
/* Default span - override with props */
|
|
40
|
+
grid-row: span 1; /* row-span-1 */
|
|
41
|
+
|
|
42
|
+
/* Full height to fill grid cell */
|
|
43
|
+
height: 100%;
|
|
44
|
+
|
|
45
|
+
/* Ensure content fills the item */
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
}
|
|
49
|
+
</style>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<motion.div
|
|
3
|
+
class="ns-entity-card"
|
|
4
|
+
:initial="{ opacity: 0, y: 20 }"
|
|
5
|
+
:animate="{ opacity: 1, y: 0 }"
|
|
6
|
+
:transition="{ duration: 0.6, ease: [0.19, 1, 0.22, 1] }"
|
|
7
|
+
>
|
|
8
|
+
<!-- Image Container -->
|
|
9
|
+
<div class="ns-entity-card__image">
|
|
10
|
+
<FlickeringGridBackground :max-opacity="0.05" color="#FFFFFF" />
|
|
11
|
+
|
|
12
|
+
<!-- Cover image if available -->
|
|
13
|
+
<EntityImage :src="imageUrl" :alt="imageAlt || 'Card image'" />
|
|
14
|
+
|
|
15
|
+
<!-- Content overlay at bottom -->
|
|
16
|
+
<div class="ns-entity-card__content">
|
|
17
|
+
<slot />
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</motion.div>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
import { motion } from "motion-v";
|
|
25
|
+
import FlickeringGridBackground from "../internal/FlickeringGridBackground.vue";
|
|
26
|
+
import EntityImage from "./EntityImage.vue";
|
|
27
|
+
|
|
28
|
+
interface Props {
|
|
29
|
+
/** Image URL (optional) */
|
|
30
|
+
imageUrl?: string;
|
|
31
|
+
|
|
32
|
+
/** Image alt text */
|
|
33
|
+
imageAlt?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
defineProps<Props>();
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<style lang="scss" scoped>
|
|
40
|
+
/**
|
|
41
|
+
* Entity Card Component
|
|
42
|
+
* Glassy card with image container and text overlay
|
|
43
|
+
* Uses New Schools SDK tokens (--ns-*)
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
.ns-entity-card {
|
|
47
|
+
/* Glassmorphism effect - same as OrganizationHero glass container */
|
|
48
|
+
background: rgba(255, 255, 255, 0.08);
|
|
49
|
+
backdrop-filter: blur(20px);
|
|
50
|
+
border-radius: var(--ns-border-radius-md);
|
|
51
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
52
|
+
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
|
53
|
+
|
|
54
|
+
/* Remove padding - let image fill */
|
|
55
|
+
padding: var(--ns-spacing-md);
|
|
56
|
+
width: 100%;
|
|
57
|
+
height: 100%; /* Fill parent height for bento grid */
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
|
|
60
|
+
/* Smooth interactions */
|
|
61
|
+
transition: all 0.3s cubic-bezier(0.19, 1, 0.22, 1);
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
|
|
64
|
+
/* Hover effect */
|
|
65
|
+
&:hover {
|
|
66
|
+
background: rgba(255, 255, 255, 0.12);
|
|
67
|
+
border-color: rgba(255, 255, 255, 0.25);
|
|
68
|
+
box-shadow: 0 12px 48px 0 rgba(0, 0, 0, 0.15);
|
|
69
|
+
transform: translateY(-4px);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Active state */
|
|
73
|
+
&:active {
|
|
74
|
+
transform: translateY(-2px);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ============================================ */
|
|
79
|
+
/* IMAGE CONTAINER */
|
|
80
|
+
/* ============================================ */
|
|
81
|
+
|
|
82
|
+
.ns-entity-card__image {
|
|
83
|
+
position: relative;
|
|
84
|
+
width: 100%;
|
|
85
|
+
height: 100%; /* Fill parent height (works for bento grid cells) */
|
|
86
|
+
border-radius: var(--ns-border-radius-md);
|
|
87
|
+
min-height: clamp(
|
|
88
|
+
280px,
|
|
89
|
+
50dvh,
|
|
90
|
+
600px
|
|
91
|
+
); /* Responsive to viewport height on mobile */
|
|
92
|
+
|
|
93
|
+
@media (min-width: 768px) {
|
|
94
|
+
min-height: auto; /* Remove min-height on tablet+ */
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Layout */
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: flex-end;
|
|
100
|
+
justify-content: flex-start;
|
|
101
|
+
|
|
102
|
+
/* Default gradient background */
|
|
103
|
+
background: linear-gradient(
|
|
104
|
+
180deg,
|
|
105
|
+
var(--ns-color-grey-900, #2d3748) 0%,
|
|
106
|
+
var(--ns-color-black, #1a202c) 100%
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* ============================================ */
|
|
111
|
+
/* CONTENT OVERLAY (BOTTOM) */
|
|
112
|
+
/* ============================================ */
|
|
113
|
+
|
|
114
|
+
.ns-entity-card__content {
|
|
115
|
+
padding: var(--ns-spacing-lg);
|
|
116
|
+
|
|
117
|
+
/* White text - same as OrganizationHero */
|
|
118
|
+
color: var(--ns-color-white);
|
|
119
|
+
|
|
120
|
+
/* Text shadow for readability */
|
|
121
|
+
& > * {
|
|
122
|
+
margin: 0;
|
|
123
|
+
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
</style>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="ns-entity-image">
|
|
3
|
+
<!-- Image with blur layers when src is provided -->
|
|
4
|
+
<template v-if="src">
|
|
5
|
+
<picture class="ns-entity-image__picture">
|
|
6
|
+
<img :src="src" :alt="alt" class="ns-entity-image__img" />
|
|
7
|
+
</picture>
|
|
8
|
+
|
|
9
|
+
<!-- Progressive blur layers for smooth gradient effect -->
|
|
10
|
+
<div
|
|
11
|
+
v-for="index in blurLayers"
|
|
12
|
+
:key="index"
|
|
13
|
+
class="ns-entity-image__blur-layer"
|
|
14
|
+
:style="getBlurLayerStyle(index)"
|
|
15
|
+
/>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<!-- Broken image icon when no src provided -->
|
|
19
|
+
<div v-else class="ns-entity-image__no-image">
|
|
20
|
+
<BrokenImageIcon />
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import BrokenImageIcon from "../internal/icons/BrokenImageIcon.vue";
|
|
27
|
+
|
|
28
|
+
interface Props {
|
|
29
|
+
/** Image source URL (optional - shows BrokenImageIcon if not provided) */
|
|
30
|
+
src?: string | null;
|
|
31
|
+
/** Image alt text */
|
|
32
|
+
alt: string;
|
|
33
|
+
/** Number of blur layers for progressive effect (default: 5) */
|
|
34
|
+
blurLayers?: number;
|
|
35
|
+
/** Blur intensity multiplier (default: 0.2) */
|
|
36
|
+
blurIntensity?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
40
|
+
blurLayers: 5,
|
|
41
|
+
blurIntensity: 0.2,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const segmentSize = 1 / (props.blurLayers + 1);
|
|
45
|
+
|
|
46
|
+
const getBlurLayerStyle = (index: number) => {
|
|
47
|
+
const gradientStops = [
|
|
48
|
+
(index - 1) * segmentSize,
|
|
49
|
+
index * segmentSize,
|
|
50
|
+
(index + 1) * segmentSize,
|
|
51
|
+
(index + 2) * segmentSize,
|
|
52
|
+
].map((pos, posIndex) => {
|
|
53
|
+
const opacity = posIndex === 1 || posIndex === 2 ? 1 : 0;
|
|
54
|
+
return `rgba(255, 255, 255, ${opacity}) ${pos * 100}%`;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
maskImage: `linear-gradient(180deg, ${gradientStops.join(", ")})`,
|
|
59
|
+
WebkitMaskImage: `linear-gradient(180deg, ${gradientStops.join(", ")})`,
|
|
60
|
+
backdropFilter: `blur(${(index - 1) * props.blurIntensity}px)`,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<style scoped>
|
|
66
|
+
/**
|
|
67
|
+
* Entity Image Component Styles
|
|
68
|
+
* Reusable image component with progressive blur layers
|
|
69
|
+
* and gradient overlay for text visibility
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
.ns-entity-image {
|
|
73
|
+
position: absolute;
|
|
74
|
+
top: 0;
|
|
75
|
+
left: 0;
|
|
76
|
+
width: 100%;
|
|
77
|
+
height: 100%;
|
|
78
|
+
z-index: 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.ns-entity-image__picture {
|
|
82
|
+
position: absolute;
|
|
83
|
+
top: 0;
|
|
84
|
+
left: 0;
|
|
85
|
+
width: 100%;
|
|
86
|
+
height: 100%;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Dark gradient overlay for text visibility */
|
|
90
|
+
.ns-entity-image__picture::after {
|
|
91
|
+
content: "";
|
|
92
|
+
position: absolute;
|
|
93
|
+
inset: 0;
|
|
94
|
+
background: linear-gradient(
|
|
95
|
+
to bottom,
|
|
96
|
+
transparent 0%,
|
|
97
|
+
transparent 30%,
|
|
98
|
+
rgba(0, 0, 0, 0.2) 60%,
|
|
99
|
+
rgba(0, 0, 0, 0.7) 100%
|
|
100
|
+
);
|
|
101
|
+
pointer-events: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.ns-entity-image__blur-layer {
|
|
105
|
+
position: absolute;
|
|
106
|
+
inset: 0;
|
|
107
|
+
pointer-events: none;
|
|
108
|
+
z-index: 2;
|
|
109
|
+
border-radius: inherit;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.ns-entity-image__img {
|
|
113
|
+
width: 100%;
|
|
114
|
+
height: 100%;
|
|
115
|
+
object-fit: cover;
|
|
116
|
+
object-position: center;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.ns-entity-image__no-image {
|
|
120
|
+
position: absolute;
|
|
121
|
+
top: var(--ns-spacing-md);
|
|
122
|
+
right: var(--ns-spacing-md);
|
|
123
|
+
z-index: 3;
|
|
124
|
+
color: var(--ns-color-grey-700);
|
|
125
|
+
pointer-events: none;
|
|
126
|
+
}
|
|
127
|
+
</style>
|
|
@@ -8,26 +8,14 @@
|
|
|
8
8
|
>
|
|
9
9
|
<!-- Image Container (Left) -->
|
|
10
10
|
<div class="ns-org-hero__image">
|
|
11
|
-
|
|
12
|
-
<slot name="background" v-if="$slots['background']" />
|
|
11
|
+
<FlickeringGridBackground :max-opacity="0.05" color="#FFFFFF" />
|
|
13
12
|
|
|
14
13
|
<!-- Cover image if available -->
|
|
15
|
-
<
|
|
16
|
-
<img
|
|
17
|
-
:src="coverImageUrl"
|
|
18
|
-
:alt="imageAlt || `${name} cover image`"
|
|
19
|
-
class="ns-org-hero__image-img"
|
|
20
|
-
/>
|
|
21
|
-
</picture>
|
|
22
|
-
|
|
23
|
-
<!-- No image fallback -->
|
|
24
|
-
<div v-else class="ns-org-hero__image--no-image">
|
|
25
|
-
<slot name="no-image" v-if="$slots['no-image']" />
|
|
26
|
-
</div>
|
|
14
|
+
<EntityImage :src="org.coverImageUrl" :alt="`${org.name} cover image`" />
|
|
27
15
|
|
|
28
16
|
<!-- Organisation name overlay -->
|
|
29
17
|
<div class="ns-org-hero__image-content">
|
|
30
|
-
<h1 class="ns-org-hero__image-title">{{ name }}</h1>
|
|
18
|
+
<h1 class="ns-org-hero__image-title">{{ org.name }}</h1>
|
|
31
19
|
|
|
32
20
|
<p class="ns-org-hero__sub-title">
|
|
33
21
|
<slot name="sub-title" v-if="$slots['sub-title']" />
|
|
@@ -44,19 +32,13 @@
|
|
|
44
32
|
|
|
45
33
|
<script setup lang="ts">
|
|
46
34
|
import { motion } from "motion-v";
|
|
35
|
+
import FlickeringGridBackground from "../internal/FlickeringGridBackground.vue";
|
|
36
|
+
import EntityImage from "./EntityImage.vue";
|
|
37
|
+
import type { Organisation } from "../../types/entities";
|
|
47
38
|
|
|
48
39
|
interface Props {
|
|
49
|
-
/** Organization
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/** Cover image URL (optional) */
|
|
53
|
-
coverImageUrl?: string;
|
|
54
|
-
|
|
55
|
-
/** Image alt text (defaults to "{name} cover image") */
|
|
56
|
-
imageAlt?: string;
|
|
57
|
-
|
|
58
|
-
/** Optional tagline/subtitle */
|
|
59
|
-
tagline?: string;
|
|
40
|
+
/** Organization object */
|
|
41
|
+
org: Organisation;
|
|
60
42
|
}
|
|
61
43
|
|
|
62
44
|
defineProps<Props>();
|
|
@@ -116,8 +98,8 @@ defineProps<Props>();
|
|
|
116
98
|
|
|
117
99
|
background: linear-gradient(
|
|
118
100
|
180deg,
|
|
119
|
-
var(--ns-color-grey-
|
|
120
|
-
var(--ns-color-
|
|
101
|
+
var(--ns-color-grey-900, #2d3748) 0%,
|
|
102
|
+
var(--ns-color-black, #1a202c) 100%
|
|
121
103
|
);
|
|
122
104
|
|
|
123
105
|
/* Layout */
|
|
@@ -134,22 +116,6 @@ defineProps<Props>();
|
|
|
134
116
|
}
|
|
135
117
|
}
|
|
136
118
|
|
|
137
|
-
.ns-org-hero__image-picture {
|
|
138
|
-
position: absolute;
|
|
139
|
-
top: 0;
|
|
140
|
-
left: 0;
|
|
141
|
-
width: 100%;
|
|
142
|
-
height: 100%;
|
|
143
|
-
z-index: 2;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
.ns-org-hero__image-img {
|
|
147
|
-
width: 100%;
|
|
148
|
-
height: 100%;
|
|
149
|
-
object-fit: cover;
|
|
150
|
-
object-position: center;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
119
|
.ns-org-hero__image--no-image {
|
|
154
120
|
position: absolute;
|
|
155
121
|
top: 50%;
|
|
@@ -25,11 +25,12 @@ import type { Ref } from "vue";
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Organization brand colors interface
|
|
28
|
+
* Colors can be null when cleared by admin (reverts to SDK defaults)
|
|
28
29
|
*/
|
|
29
30
|
interface OrganizationBrandColors {
|
|
30
|
-
primary?: string;
|
|
31
|
-
secondary?: string;
|
|
32
|
-
tertiary?: string;
|
|
31
|
+
primary?: string | null;
|
|
32
|
+
secondary?: string | null;
|
|
33
|
+
tertiary?: string | null;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { computed, onBeforeUnmount, onMounted, ref, toRefs } from "vue";
|
|
3
|
+
|
|
4
|
+
interface FlickeringGridProps {
|
|
5
|
+
squareSize?: number;
|
|
6
|
+
gridGap?: number;
|
|
7
|
+
flickerChance?: number;
|
|
8
|
+
color?: string;
|
|
9
|
+
width?: number;
|
|
10
|
+
height?: number;
|
|
11
|
+
class?: string;
|
|
12
|
+
maxOpacity?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const props = withDefaults(defineProps<FlickeringGridProps>(), {
|
|
16
|
+
squareSize: 4,
|
|
17
|
+
gridGap: 6,
|
|
18
|
+
flickerChance: 0.3,
|
|
19
|
+
color: "rgb(0, 0, 0)",
|
|
20
|
+
maxOpacity: 0.3,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const { squareSize, gridGap, flickerChance, color, maxOpacity, width, height } =
|
|
24
|
+
toRefs(props);
|
|
25
|
+
|
|
26
|
+
const containerRef = ref<HTMLDivElement>();
|
|
27
|
+
const canvasRef = ref<HTMLCanvasElement>();
|
|
28
|
+
const context = ref<CanvasRenderingContext2D>();
|
|
29
|
+
|
|
30
|
+
const isInView = ref(false);
|
|
31
|
+
const canvasSize = ref({ width: 0, height: 0 });
|
|
32
|
+
|
|
33
|
+
const computedColor = computed(() => {
|
|
34
|
+
if (!context.value) return "rgba(255, 0, 0,";
|
|
35
|
+
|
|
36
|
+
const hex = color.value.replace(/^#/, "");
|
|
37
|
+
const bigint = Number.parseInt(hex, 16);
|
|
38
|
+
const r = (bigint >> 16) & 255;
|
|
39
|
+
const g = (bigint >> 8) & 255;
|
|
40
|
+
const b = bigint & 255;
|
|
41
|
+
return `rgba(${r}, ${g}, ${b},`;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function setupCanvas(
|
|
45
|
+
canvas: HTMLCanvasElement,
|
|
46
|
+
width: number,
|
|
47
|
+
height: number,
|
|
48
|
+
): {
|
|
49
|
+
cols: number;
|
|
50
|
+
rows: number;
|
|
51
|
+
squares: Float32Array;
|
|
52
|
+
dpr: number;
|
|
53
|
+
} {
|
|
54
|
+
const dpr = window.devicePixelRatio || 1;
|
|
55
|
+
canvas.width = width * dpr;
|
|
56
|
+
canvas.height = height * dpr;
|
|
57
|
+
canvas.style.width = `${width}px`;
|
|
58
|
+
canvas.style.height = `${height}px`;
|
|
59
|
+
|
|
60
|
+
const cols = Math.floor(width / (squareSize.value + gridGap.value));
|
|
61
|
+
const rows = Math.floor(height / (squareSize.value + gridGap.value));
|
|
62
|
+
|
|
63
|
+
const squares = new Float32Array(cols * rows);
|
|
64
|
+
for (let i = 0; i < squares.length; i++) {
|
|
65
|
+
squares[i] = Math.random() * maxOpacity.value;
|
|
66
|
+
}
|
|
67
|
+
return { cols, rows, squares, dpr };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function updateSquares(squares: Float32Array, deltaTime: number) {
|
|
71
|
+
for (let i = 0; i < squares.length; i++) {
|
|
72
|
+
if (Math.random() < flickerChance.value * deltaTime) {
|
|
73
|
+
squares[i] = Math.random() * maxOpacity.value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function drawGrid(
|
|
79
|
+
ctx: CanvasRenderingContext2D,
|
|
80
|
+
width: number,
|
|
81
|
+
height: number,
|
|
82
|
+
cols: number,
|
|
83
|
+
rows: number,
|
|
84
|
+
squares: Float32Array,
|
|
85
|
+
dpr: number,
|
|
86
|
+
) {
|
|
87
|
+
ctx.clearRect(0, 0, width, height);
|
|
88
|
+
ctx.fillStyle = "transparent";
|
|
89
|
+
ctx.fillRect(0, 0, width, height);
|
|
90
|
+
for (let i = 0; i < cols; i++) {
|
|
91
|
+
for (let j = 0; j < rows; j++) {
|
|
92
|
+
const opacity = squares[i * rows + j];
|
|
93
|
+
ctx.fillStyle = `${computedColor.value}${opacity})`;
|
|
94
|
+
ctx.fillRect(
|
|
95
|
+
i * (squareSize.value + gridGap.value) * dpr,
|
|
96
|
+
j * (squareSize.value + gridGap.value) * dpr,
|
|
97
|
+
squareSize.value * dpr,
|
|
98
|
+
squareSize.value * dpr,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const gridParams = ref<ReturnType<typeof setupCanvas>>();
|
|
105
|
+
|
|
106
|
+
function updateCanvasSize() {
|
|
107
|
+
const newWidth = width.value || containerRef.value!.clientWidth;
|
|
108
|
+
const newHeight = height.value || containerRef.value!.clientHeight;
|
|
109
|
+
|
|
110
|
+
canvasSize.value = { width: newWidth, height: newHeight };
|
|
111
|
+
gridParams.value = setupCanvas(canvasRef.value!, newWidth, newHeight);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let animationFrameId: number | undefined;
|
|
115
|
+
let resizeObserver: ResizeObserver | undefined;
|
|
116
|
+
let intersectionObserver: IntersectionObserver | undefined;
|
|
117
|
+
let lastTime = 0;
|
|
118
|
+
|
|
119
|
+
function animate(time: number) {
|
|
120
|
+
if (!isInView.value) return;
|
|
121
|
+
|
|
122
|
+
const deltaTime = (time - lastTime) / 1000;
|
|
123
|
+
lastTime = time;
|
|
124
|
+
|
|
125
|
+
updateSquares(gridParams.value!.squares, deltaTime);
|
|
126
|
+
drawGrid(
|
|
127
|
+
context.value!,
|
|
128
|
+
canvasRef.value!.width,
|
|
129
|
+
canvasRef.value!.height,
|
|
130
|
+
gridParams.value!.cols,
|
|
131
|
+
gridParams.value!.rows,
|
|
132
|
+
gridParams.value!.squares,
|
|
133
|
+
gridParams.value!.dpr,
|
|
134
|
+
);
|
|
135
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
onMounted(() => {
|
|
139
|
+
if (!canvasRef.value || !containerRef.value) return;
|
|
140
|
+
context.value = canvasRef.value.getContext("2d")!;
|
|
141
|
+
if (!context.value) return;
|
|
142
|
+
|
|
143
|
+
updateCanvasSize();
|
|
144
|
+
|
|
145
|
+
resizeObserver = new ResizeObserver(() => {
|
|
146
|
+
updateCanvasSize();
|
|
147
|
+
});
|
|
148
|
+
intersectionObserver = new IntersectionObserver(
|
|
149
|
+
([entry]) => {
|
|
150
|
+
isInView.value = entry.isIntersecting;
|
|
151
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
152
|
+
},
|
|
153
|
+
{ threshold: 0 },
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
resizeObserver.observe(containerRef.value);
|
|
157
|
+
intersectionObserver.observe(canvasRef.value);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
onBeforeUnmount(() => {
|
|
161
|
+
if (animationFrameId) {
|
|
162
|
+
cancelAnimationFrame(animationFrameId);
|
|
163
|
+
}
|
|
164
|
+
resizeObserver?.disconnect();
|
|
165
|
+
intersectionObserver?.disconnect();
|
|
166
|
+
});
|
|
167
|
+
</script>
|
|
168
|
+
|
|
169
|
+
<template>
|
|
170
|
+
<div
|
|
171
|
+
ref="containerRef"
|
|
172
|
+
class="flickering-grid-container"
|
|
173
|
+
:class="[props.class]"
|
|
174
|
+
>
|
|
175
|
+
<canvas
|
|
176
|
+
ref="canvasRef"
|
|
177
|
+
class="flickering-grid-canvas"
|
|
178
|
+
:width="canvasSize.width"
|
|
179
|
+
:height="canvasSize.height"
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
</template>
|
|
183
|
+
|
|
184
|
+
<style scoped>
|
|
185
|
+
.flickering-grid-container {
|
|
186
|
+
/* Position absolutely to fill parent without affecting layout */
|
|
187
|
+
position: absolute;
|
|
188
|
+
top: 0;
|
|
189
|
+
left: 0;
|
|
190
|
+
width: 100%;
|
|
191
|
+
height: 100%;
|
|
192
|
+
z-index: 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.flickering-grid-canvas {
|
|
196
|
+
pointer-events: none;
|
|
197
|
+
}
|
|
198
|
+
</style>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg
|
|
3
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
4
|
+
:height="height"
|
|
5
|
+
:viewBox="viewBox"
|
|
6
|
+
:width="width"
|
|
7
|
+
fill="currentColor"
|
|
8
|
+
>
|
|
9
|
+
<path
|
|
10
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
11
|
+
d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm40-337 160-160 160 160 160-160 40 40v-183H200v263l40 40Zm-40 257h560v-264l-40-40-160 160-160-160-160 160-40-40v184Zm0 0v-264 80-376 560Z"
|
|
12
|
+
/>
|
|
13
|
+
</svg>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup>
|
|
17
|
+
defineProps({
|
|
18
|
+
width: {
|
|
19
|
+
type: [Number, String],
|
|
20
|
+
default: 24,
|
|
21
|
+
},
|
|
22
|
+
height: {
|
|
23
|
+
type: [Number, String],
|
|
24
|
+
default: 24,
|
|
25
|
+
},
|
|
26
|
+
viewBox: {
|
|
27
|
+
type: String,
|
|
28
|
+
default: "0 -960 960 960",
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
@@ -11,12 +11,13 @@ import { defineNuxtPlugin, useState, useRuntimeConfig } from "nuxt/app";
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Apply organization brand colors to document root
|
|
14
|
-
* Sets CSS custom properties that override SDK defaults
|
|
14
|
+
* Sets CSS custom properties that override SDK defaults from tokens.css
|
|
15
|
+
* When color is null, removes the override to revert to default
|
|
15
16
|
*/
|
|
16
17
|
function applyOrganizationTheme(colors?: {
|
|
17
|
-
primary?: string;
|
|
18
|
-
secondary?: string;
|
|
19
|
-
tertiary?: string;
|
|
18
|
+
primary?: string | null;
|
|
19
|
+
secondary?: string | null;
|
|
20
|
+
tertiary?: string | null;
|
|
20
21
|
}) {
|
|
21
22
|
if (!colors) return;
|
|
22
23
|
|
|
@@ -24,17 +25,22 @@ function applyOrganizationTheme(colors?: {
|
|
|
24
25
|
if (import.meta.client) {
|
|
25
26
|
const root = document.documentElement;
|
|
26
27
|
|
|
28
|
+
// Primary: set override if string, remove override if null (reverts to #4a3f8f)
|
|
27
29
|
if (colors.primary) {
|
|
28
30
|
root.style.setProperty("--ns-color-primary", colors.primary);
|
|
29
31
|
}
|
|
32
|
+
|
|
33
|
+
// Secondary: set override if string, remove override if null (reverts to #ff6b6b)
|
|
30
34
|
if (colors.secondary) {
|
|
31
35
|
root.style.setProperty("--ns-color-secondary", colors.secondary);
|
|
32
36
|
}
|
|
37
|
+
|
|
38
|
+
// Tertiary: set override if string, remove override if null (reverts to #0fa3b1)
|
|
33
39
|
if (colors.tertiary) {
|
|
34
40
|
root.style.setProperty("--ns-color-tertiary", colors.tertiary);
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
if (
|
|
43
|
+
if (import.meta.dev) {
|
|
38
44
|
console.log("[NewSchools] Theme applied:", colors);
|
|
39
45
|
}
|
|
40
46
|
}
|
|
@@ -6,6 +6,84 @@
|
|
|
6
6
|
* Internal implementation details (moderation, Firebase paths, etc.) are omitted.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Organisation Status Type
|
|
11
|
+
* - active: Normal operation
|
|
12
|
+
* - expired: Demo period ended
|
|
13
|
+
* - suspended: Manually disabled
|
|
14
|
+
* - deleted: Soft-deleted
|
|
15
|
+
*/
|
|
16
|
+
export type OrganisationStatus = "active" | "expired" | "suspended" | "deleted";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Organisation Type
|
|
20
|
+
* - regular: Standard paid/permanent organisation
|
|
21
|
+
* - demo: Time-limited trial organisation
|
|
22
|
+
*/
|
|
23
|
+
export type OrganisationType = "demo" | "regular";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Organisation Visibility
|
|
27
|
+
* - private: Only accessible to members
|
|
28
|
+
* - public: Public landing page accessible
|
|
29
|
+
*/
|
|
30
|
+
export type OrganisationVisibility = "private" | "public";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Organisation Brand Customization
|
|
34
|
+
* Used for public landing pages and SDK theming
|
|
35
|
+
*/
|
|
36
|
+
export interface OrganisationBrand {
|
|
37
|
+
/** Brand color palette (hex values) */
|
|
38
|
+
colors?: {
|
|
39
|
+
/** Primary brand color (hex, e.g., "#ff0000") - null to clear */
|
|
40
|
+
primary?: string | null;
|
|
41
|
+
|
|
42
|
+
/** Secondary brand color (hex, e.g., "#00ff00") - null to clear */
|
|
43
|
+
secondary?: string | null;
|
|
44
|
+
|
|
45
|
+
/** Tertiary brand color (hex, e.g., "#0000ff") - null to clear */
|
|
46
|
+
tertiary?: string | null;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Organisation Entity
|
|
52
|
+
* Represents an academy, business, or institution
|
|
53
|
+
* Public-facing fields only (excludes internal metadata)
|
|
54
|
+
*/
|
|
55
|
+
export interface Organisation {
|
|
56
|
+
/** Unique identifier */
|
|
57
|
+
id: string;
|
|
58
|
+
|
|
59
|
+
/** Display name */
|
|
60
|
+
name: string;
|
|
61
|
+
|
|
62
|
+
/** URL-safe unique slug */
|
|
63
|
+
slug: string;
|
|
64
|
+
|
|
65
|
+
/** Current status */
|
|
66
|
+
status: OrganisationStatus;
|
|
67
|
+
|
|
68
|
+
/** Type determines features and lifecycle */
|
|
69
|
+
type: OrganisationType;
|
|
70
|
+
|
|
71
|
+
/** Controls public landing page access */
|
|
72
|
+
visibility: OrganisationVisibility;
|
|
73
|
+
|
|
74
|
+
/** ISO timestamp of creation */
|
|
75
|
+
createdAt: string;
|
|
76
|
+
|
|
77
|
+
/** ISO timestamp for demo expiration (demo type only) */
|
|
78
|
+
expiresAt?: string;
|
|
79
|
+
|
|
80
|
+
/** Public URL of cover image */
|
|
81
|
+
coverImageUrl?: string;
|
|
82
|
+
|
|
83
|
+
/** Brand customization (colors, logos, etc.) */
|
|
84
|
+
brand?: OrganisationBrand;
|
|
85
|
+
}
|
|
86
|
+
|
|
9
87
|
/**
|
|
10
88
|
* Journey Theme Type
|
|
11
89
|
* Available theme options that determine UI terminology
|
package/package.json
CHANGED