@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.
@@ -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
- <!-- Flickering grid background effect -->
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
- <picture v-if="coverImageUrl" class="ns-org-hero__image-picture">
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 name */
50
- name: string;
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-800, #2d3748) 0%,
120
- var(--ns-color-grey-900, #1a202c) 100%
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 (process.dev) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newschools/sdk",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "New Schools SDK - Multi-framework components and modules for integrating New Schools learning content",
5
5
  "type": "module",
6
6
  "main": "./nuxt/src/module.ts",