@newschools/sdk 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -1,29 +1,30 @@
1
- # @newschools/nuxt
1
+ # @newschools/sdk
2
2
 
3
- Nuxt module for integrating New Schools learning content into your applications.
3
+ Nuxt module for integrating New Schools learning content into your applications with automatic organization theming.
4
4
 
5
5
  ## Features
6
6
 
7
- ✨ **Auto-imported composables** - `useAsyncNewSchools` available everywhere
7
+ ✨ **Auto-imported composables** - `useAsyncNewSchools` and `useOrganization` available everywhere
8
+ 🎨 **Automatic theming** - Organization brand colors applied as CSS variables
8
9
  🔒 **Type-safe** - Full TypeScript support with entity types
9
10
  ⚡ **SSR-compatible** - Built on Nuxt's `useAsyncData`
10
- 🎯 **Simple API** - Fetch journeys, waypoints, and activities with ease
11
- 🔑 **API key authentication** - Secure access to your organisation's content
11
+ 🎯 **Ready-to-use components** - OrganizationHero, BentoGrid, EntityCard
12
+ 🔑 **API key authentication** - Secure access to your organization's content
12
13
 
13
14
  ## Installation
14
15
 
15
16
  ```bash
16
17
  # Using npm
17
- npm install @newschools/nuxt
18
+ npm install @newschools/sdk
18
19
 
19
20
  # Using yarn
20
- yarn add @newschools/nuxt
21
+ yarn add @newschools/sdk
21
22
 
22
23
  # Using pnpm
23
- pnpm add @newschools/nuxt
24
+ pnpm add @newschools/sdk
24
25
 
25
26
  # Using bun
26
- bun add @newschools/nuxt
27
+ bun add @newschools/sdk
27
28
  ```
28
29
 
29
30
  ## Setup
@@ -32,7 +33,12 @@ bun add @newschools/nuxt
32
33
 
33
34
  ```typescript
34
35
  export default defineNuxtConfig({
35
- modules: ["@newschools/nuxt"],
36
+ modules: ["@newschools/sdk"],
37
+
38
+ newschools: {
39
+ apiKey: process.env.NUXT_PUBLIC_NEWSCHOOLS_API_KEY,
40
+ organizationSlug: "my-organization", // Optional: auto-fetch org data and apply brand colors
41
+ },
36
42
  });
37
43
  ```
38
44
 
@@ -44,175 +50,314 @@ Add your New Schools API key to `.env`:
44
50
  NUXT_PUBLIC_NEWSCHOOLS_API_KEY=ns_live_xxxxxxxxxxxxx
45
51
  ```
46
52
 
47
- Or configure directly in `nuxt.config.ts`:
53
+ ### 3. (Optional) Configure organization
54
+
55
+ If you want to automatically load organization data and apply brand theming:
48
56
 
49
57
  ```typescript
50
58
  export default defineNuxtConfig({
51
- modules: ["@newschools/nuxt"],
59
+ modules: ["@newschools/sdk"],
52
60
 
53
61
  newschools: {
54
- apiKey: "ns_live_xxxxxxxxxxxxx",
55
- baseUrl: "https://newschools.ai", // optional, defaults to production
62
+ apiKey: process.env.NUXT_PUBLIC_NEWSCHOOLS_API_KEY,
63
+ organizationSlug: "webdesignwill", // Loads org data on init
56
64
  },
57
65
  });
58
66
  ```
59
67
 
68
+ When `organizationSlug` is set, the SDK will:
69
+
70
+ - ✅ Auto-fetch organization data on app initialization
71
+ - ✅ Apply brand colors (`primary`, `secondary`, `tertiary`) as CSS variables
72
+ - ✅ Make organization data available via `useOrganization()` composable
73
+
74
+ ## Automatic Brand Theming
75
+
76
+ The SDK automatically applies your organization's brand colors as CSS variables:
77
+
78
+ ```typescript
79
+ // In nuxt.config.ts
80
+ export default defineNuxtConfig({
81
+ newschools: {
82
+ organizationSlug: "my-org", // Has primary: "#FF0000"
83
+ },
84
+ });
85
+ ```
86
+
87
+ The SDK sets:
88
+
89
+ - `--ns-color-primary` → `#FF0000`
90
+ - `--ns-color-secondary` → (your secondary color)
91
+ - `--ns-color-tertiary` → (your tertiary color)
92
+
93
+ If colors are not set or cleared by admin, they revert to SDK defaults:
94
+
95
+ - `--ns-color-primary` → `#4a3f8f` (Deep Purple)
96
+ - `--ns-color-secondary` → `#ff6b6b` (Vibrant Coral)
97
+ - `--ns-color-tertiary` → `#0fa3b1` (Rich Teal)
98
+
99
+ Use these variables in your components:
100
+
101
+ ```vue
102
+ <template>
103
+ <div class="my-component">
104
+ <h1>Automatically themed!</h1>
105
+ </div>
106
+ </template>
107
+
108
+ <style scoped>
109
+ .my-component {
110
+ background: var(--ns-color-primary);
111
+ color: white;
112
+ }
113
+
114
+ h1 {
115
+ border-bottom: 2px solid var(--ns-color-secondary);
116
+ }
117
+ </style>
118
+ ```
119
+
60
120
  ## Usage
61
121
 
62
- The `useAsyncNewSchools` composable is auto-imported and available in all components, pages, and composables.
122
+ ### Organization Data
63
123
 
64
- ### List Journeys
124
+ Access organization data loaded by the SDK:
65
125
 
66
126
  ```vue
67
127
  <script setup lang="ts">
68
- const {
69
- data: journeys,
70
- pending,
71
- error,
72
- refresh,
73
- } = await useAsyncNewSchools("journeys");
128
+ const organization = useOrganization();
74
129
  </script>
75
130
 
76
131
  <template>
77
- <div>
78
- <div v-if="pending">Loading journeys...</div>
79
- <div v-else-if="error">Error: {{ error.message }}</div>
80
- <div v-else>
81
- <div v-for="journey in journeys" :key="journey.id">
82
- <h2>{{ journey.title }}</h2>
83
- <p>{{ journey.description }}</p>
84
- </div>
85
- </div>
132
+ <div v-if="organization">
133
+ <h1>{{ organization.name }}</h1>
134
+ <img v-if="organization.coverImageUrl" :src="organization.coverImageUrl" />
86
135
  </div>
87
136
  </template>
88
137
  ```
89
138
 
90
- ### Get Single Journey
139
+ ### Pre-built Components
140
+
141
+ #### OrganizationHero
142
+
143
+ Display organization header with cover image and branding:
91
144
 
92
145
  ```vue
93
- <script setup lang="ts">
94
- const route = useRoute();
95
- const slug = route.params.slug as string;
146
+ <template>
147
+ <OrganizationHero :org="organization">
148
+ <template #sub-title>
149
+ <p>Welcome to our learning platform</p>
150
+ </template>
151
+
152
+ <template #content>
153
+ <div>Custom content area for CTAs, forms, etc.</div>
154
+ </template>
155
+ </OrganizationHero>
156
+ </template>
96
157
 
97
- const { data: journey } = await useAsyncNewSchools("journey", { id: slug });
158
+ <script setup lang="ts">
159
+ const organization = useOrganization();
98
160
  </script>
161
+ ```
162
+
163
+ #### BentoGrid
164
+
165
+ Create responsive masonry-style layouts:
99
166
 
167
+ ```vue
100
168
  <template>
101
- <div>
102
- <h1>{{ journey?.title }}</h1>
103
- <p>{{ journey?.summary }}</p>
104
- <img v-if="journey?.coverImageUrl" :src="journey.coverImageUrl" />
105
- </div>
169
+ <BentoGrid>
170
+ <!-- 2x2 Featured card -->
171
+ <BentoGridItem :col-span="2" :row-span="2">
172
+ <EntityCard>
173
+ <h3>Featured Content</h3>
174
+ </EntityCard>
175
+ </BentoGridItem>
176
+
177
+ <!-- Standard cards -->
178
+ <BentoGridItem>
179
+ <EntityCard>
180
+ <h3>Regular Card</h3>
181
+ </EntityCard>
182
+ </BentoGridItem>
183
+
184
+ <!-- Wide card -->
185
+ <BentoGridItem :col-span="3">
186
+ <EntityCard>
187
+ <h3>Wide Card</h3>
188
+ </EntityCard>
189
+ </BentoGridItem>
190
+ </BentoGrid>
106
191
  </template>
107
192
  ```
108
193
 
109
- ### List Waypoints for Journey
194
+ ### Fetching Learning Content
195
+
196
+ The `useAsyncNewSchools` composable is auto-imported and available everywhere.
197
+
198
+ #### Fetch all journeys
110
199
 
111
200
  ```vue
112
201
  <script setup lang="ts">
113
- const route = useRoute();
114
- const journeyId = route.params.journeyId as string;
115
-
116
- const { data: waypoints } = await useAsyncNewSchools("waypoints", {
117
- journeyId,
118
- });
202
+ const { data: journeys, error } = await useAsyncNewSchools("journeys");
119
203
  </script>
120
204
 
121
205
  <template>
122
- <div>
123
- <div v-for="waypoint in waypoints" :key="waypoint.id">
124
- <h3>{{ waypoint.title }}</h3>
206
+ <div v-if="journeys">
207
+ <div v-for="journey in journeys" :key="journey.id">
208
+ <h2>{{ journey.title }}</h2>
209
+ <p>{{ journey.description }}</p>
125
210
  </div>
126
211
  </div>
127
212
  </template>
128
213
  ```
129
214
 
130
- ### Get Single Waypoint
215
+ #### Fetch a specific journey
131
216
 
132
217
  ```vue
133
218
  <script setup lang="ts">
134
- const { data: waypoint } = await useAsyncNewSchools("waypoint", {
135
- journeyId: "journey-123",
136
- waypointId: "waypoint-456",
219
+ const route = useRoute();
220
+ const { data: journey } = await useAsyncNewSchools("journey", {
221
+ id: route.params.slug,
137
222
  });
138
223
  </script>
224
+
225
+ <template>
226
+ <div v-if="journey">
227
+ <h1>{{ journey.title }}</h1>
228
+ <p>{{ journey.description }}</p>
229
+ </div>
230
+ </template>
139
231
  ```
140
232
 
141
- ### List Activities
233
+ #### Fetch waypoints for a journey
142
234
 
143
235
  ```vue
144
236
  <script setup lang="ts">
145
- const { data: activities } = await useAsyncNewSchools("activities", {
146
- journeyId: "journey-123",
147
- waypointId: "waypoint-456",
237
+ const { data: waypoints } = await useAsyncNewSchools("waypoints", {
238
+ journeyId: "my-journey-slug",
148
239
  });
149
240
  </script>
150
241
  ```
151
242
 
152
- ### Get Single Activity
243
+ #### Fetch a specific waypoint
153
244
 
154
245
  ```vue
155
246
  <script setup lang="ts">
156
- const { data: activity } = await useAsyncNewSchools("activity", {
157
- journeyId: "journey-123",
158
- waypointId: "waypoint-456",
159
- activityId: "activity-789",
247
+ const { data: waypoint } = await useAsyncNewSchools("waypoint", {
248
+ journeyId: "journey-slug",
249
+ waypointId: "waypoint-slug",
160
250
  });
161
251
  </script>
162
252
  ```
163
253
 
164
- ## API Reference
165
-
166
- ### `useAsyncNewSchools(resource, params?)`
167
-
168
- Fetch data from New Schools v1 API with SSR support.
169
-
170
- **Parameters:**
171
-
172
- - `resource` - Resource type to fetch:
173
- - `'journeys'` - List all journeys
174
- - `'journey'` - Get single journey
175
- - `'waypoints'` - List waypoints for journey
176
- - `'waypoint'` - Get single waypoint
177
- - `'activities'` - List activities for waypoint
178
- - `'activity'` - Get single activity
179
-
180
- - `params` - Resource-specific parameters (optional for lists):
181
- - Journey: `{ id: string }`
182
- - Waypoint: `{ journeyId: string, waypointId?: string }`
183
- - Activity: `{ journeyId: string, waypointId: string, activityId?: string }`
184
-
185
- **Returns:**
186
-
187
- `AsyncData` object with:
188
-
189
- - `data` - Fetched data (reactive)
190
- - `pending` - Loading state (reactive)
191
- - `error` - Error object if request failed (reactive)
192
- - `refresh()` - Function to refetch data
193
-
194
254
  ## TypeScript Support
195
255
 
196
256
  Full type safety for all entities:
197
257
 
198
258
  ```typescript
199
259
  import type {
260
+ Organisation,
261
+ OrganisationBrand,
200
262
  Journey,
201
263
  Waypoint,
202
264
  Activity,
203
265
  JourneyTheme,
204
266
  WaypointType,
205
267
  LayoutMode,
206
- } from "@newschools/nuxt";
268
+ } from "@newschools/sdk";
269
+
270
+ // Types are automatically inferred in composables
271
+ const organization = useOrganization();
272
+ // organization is typed as Ref<Organisation | null>
207
273
 
208
- // Types are automatically inferred in useAsyncNewSchools
209
274
  const { data: journeys } = await useAsyncNewSchools("journeys");
210
275
  // journeys is typed as Journey[] | null
276
+ ```
211
277
 
212
- const { data: journey } = await useAsyncNewSchools("journey", { id: "slug" });
213
- // journey is typed as Journey | null
278
+ ## Module Configuration
279
+
280
+ All configuration options for `nuxt.config.ts`:
281
+
282
+ ```typescript
283
+ export default defineNuxtConfig({
284
+ newschools: {
285
+ /**
286
+ * New Schools API key (required)
287
+ * Can also be set via NUXT_PUBLIC_NEWSCHOOLS_API_KEY env variable
288
+ */
289
+ apiKey: string;
290
+
291
+ /**
292
+ * Base URL for New Schools API
293
+ * Default: "https://newschools.ai"
294
+ */
295
+ baseUrl?: string;
296
+
297
+ /**
298
+ * Organization slug for auto-fetch and theming
299
+ * When set, SDK will:
300
+ * - Automatically fetch organization data on init
301
+ * - Apply brand colors as CSS variables
302
+ * - Make data available via useOrganization()
303
+ *
304
+ * Example: "my-organization"
305
+ */
306
+ organizationSlug?: string;
307
+ },
308
+ });
214
309
  ```
215
310
 
311
+ ### Do you need `baseUrl`?
312
+
313
+ **No**, unless you're using a custom API endpoint or testing against a development server.
314
+
315
+ The default value is `"https://newschools.ai"`, which points to production.
316
+
317
+ ```typescript
318
+ // ✅ Minimal config (most common)
319
+ export default defineNuxtConfig({
320
+ newschools: {
321
+ apiKey: process.env.NUXT_PUBLIC_NEWSCHOOLS_API_KEY,
322
+ },
323
+ });
324
+
325
+ // ✅ With organization theming
326
+ export default defineNuxtConfig({
327
+ newschools: {
328
+ apiKey: process.env.NUXT_PUBLIC_NEWSCHOOLS_API_KEY,
329
+ organizationSlug: "webdesignwill",
330
+ },
331
+ });
332
+
333
+ // ⚠️ Only needed for custom environments
334
+ export default defineNuxtConfig({
335
+ newschools: {
336
+ apiKey: process.env.NUXT_PUBLIC_NEWSCHOOLS_API_KEY,
337
+ baseUrl: "https://staging.newschools.ai", // Testing against staging
338
+ },
339
+ });
340
+ ```
341
+
342
+ ## Available CSS Variables
343
+
344
+ The SDK provides themeable CSS tokens:
345
+
346
+ ### Brand Colors (Automatically set from organization)
347
+
348
+ - `--ns-color-primary` - Primary brand color
349
+ - `--ns-color-secondary` - Secondary brand color
350
+ - `--ns-color-tertiary` - Tertiary brand color
351
+
352
+ ### Additional Tokens
353
+
354
+ - `--ns-color-grey-800`, `--ns-color-grey-900`
355
+ - `--ns-spacing-xs` through `--ns-spacing-3xl`
356
+ - `--ns-border-radius-sm` through `--ns-border-radius-2xl`
357
+ - `--ns-shadow-sm` through `--ns-shadow-xl`
358
+
359
+ See [tokens.css](./nuxt/src/runtime/styles/tokens.css) for the complete list.
360
+
216
361
  ## Advanced Usage
217
362
 
218
363
  ### Manual Client (Without Nuxt Composables)
@@ -220,7 +365,7 @@ const { data: journey } = await useAsyncNewSchools("journey", { id: "slug" });
220
365
  For use in server routes, plugins, or non-component contexts:
221
366
 
222
367
  ```typescript
223
- import { createClient } from "@newschools/nuxt";
368
+ import { createClient } from "@newschools/sdk";
224
369
 
225
370
  const client = createClient({
226
371
  apiKey: "ns_live_xxxxxxxxxxxxx",
@@ -231,59 +376,83 @@ const journeys = await client.get<Journey[]>("/journeys");
231
376
  const journey = await client.get<Journey>("/journeys/my-slug");
232
377
  ```
233
378
 
234
- ### Custom Path Resolution
379
+ ## Environment Variables
235
380
 
236
- For advanced integrations:
381
+ | Variable | Description | Required | Default |
382
+ | --------------------------------- | ------------------------ | -------- | --------------------- |
383
+ | `NUXT_PUBLIC_NEWSCHOOLS_API_KEY` | Your New Schools API key | Yes | - |
384
+ | `NUXT_PUBLIC_NEWSCHOOLS_BASE_URL` | API base URL | No | https://newschools.ai |
237
385
 
238
- ```typescript
239
- import { NewSchoolsPathResolver } from "@newschools/nuxt";
386
+ ## Components Reference
240
387
 
241
- const path = NewSchoolsPathResolver.resolve("journey", "list");
242
- // Returns: '/journeys'
388
+ ### `<OrganizationHero>`
243
389
 
244
- const path2 = NewSchoolsPathResolver.resolve("waypoint", "get", {
245
- journeyId: "123",
246
- waypointId: "456",
247
- });
248
- // Returns: '/journeys/123/waypoints/456'
249
- ```
390
+ Display organization header with cover image and branding
250
391
 
251
- ## Local Development (Testing with bun link)
392
+ **Props:**
252
393
 
253
- For testing the SDK locally before publishing:
394
+ - `org: Organisation` - Organization object (required)
254
395
 
255
- ```bash
256
- # In the SDK package directory
257
- cd packages/nuxt
258
- bun link
396
+ **Slots:**
259
397
 
260
- # In your test project
261
- cd ~/my-test-project
262
- bun link @newschools/nuxt
263
- ```
398
+ - `sub-title` - Content below organization name
399
+ - `content` - Right-side content area
400
+ - `background` - Custom background component
264
401
 
265
- Add to your test project's `nuxt.config.ts`:
402
+ ### `<BentoGrid>`
266
403
 
267
- ```typescript
268
- export default defineNuxtConfig({
269
- modules: ["@newschools/nuxt"],
270
- });
271
- ```
404
+ Responsive masonry-style grid layout
272
405
 
273
- ## Environment Variables
406
+ ### `<BentoGridItem>`
274
407
 
275
- | Variable | Description | Required |
276
- | --------------------------------- | ------------------------------------- | -------- |
277
- | `NUXT_PUBLIC_NEWSCHOOLS_API_KEY` | Your New Schools API key | Yes |
278
- | `NUXT_PUBLIC_NEWSCHOOLS_BASE_URL` | API base URL (defaults to production) | No |
408
+ Grid item with flexible sizing
279
409
 
280
- ## License
410
+ **Props:**
281
411
 
282
- MIT
412
+ - `col-span?: number` - Column span (1-3)
413
+ - `row-span?: number` - Row span (1-2)
414
+
415
+ ### `<EntityCard>`
416
+
417
+ Card component for displaying content
418
+
419
+ **Slots:**
283
420
 
284
- ## Support
421
+ - `default` - Card content
422
+ - `background` - Background layer
285
423
 
286
- For issues or questions:
424
+ ### `<EntityImage>`
287
425
 
288
- - API documentation: https://newschools.ai/docs
289
- - Contact: support@newschools.ai
426
+ Optimized image component with loading states
427
+
428
+ **Props:**
429
+
430
+ - `src?: string` - Image URL
431
+ - `alt: string` - Alt text
432
+
433
+ ## Composables Reference
434
+
435
+ ### `useOrganization()`
436
+
437
+ Access globally loaded organization data
438
+
439
+ **Returns:** `Ref<Organisation | null>`
440
+
441
+ ### `useAsyncNewSchools(resource, params?)`
442
+
443
+ Fetch data from New Schools v1 API with SSR support
444
+
445
+ **Resources:**
446
+
447
+ - `'journeys'` - List all journeys
448
+ - `'journey'` - Get single journey
449
+ - `'waypoints'` - List waypoints
450
+ - `'waypoint'` - Get single waypoint
451
+ - `'activities'` - List activities
452
+ - `'activity'` - Get single activity
453
+
454
+ **Returns:** `AsyncData<T>`
455
+
456
+ ## License
457
+
458
+ MIT
@@ -14,7 +14,7 @@ const props = defineProps<Props>();
14
14
  </div>
15
15
  </template>
16
16
 
17
- <style lang="scss" scoped>
17
+ <style scoped>
18
18
  /**
19
19
  * Bento Grid Component
20
20
  * Asymmetric grid layout inspired by Japanese bento boxes
@@ -27,9 +27,11 @@ const props = defineProps<Props>();
27
27
  display: flex;
28
28
  flex-direction: column;
29
29
  gap: var(--ns-spacing-md);
30
+ }
30
31
 
31
- /* Tablet and up: 3 column bento grid with auto rows */
32
- @media (min-width: 768px) {
32
+ /* Tablet and up: 3 column bento grid with auto rows */
33
+ @media (min-width: 768px) {
34
+ .ns-bento-grid {
33
35
  display: grid;
34
36
  grid-template-columns: repeat(3, 1fr);
35
37
  grid-auto-rows: 18rem;
@@ -7,12 +7,32 @@ interface Props {
7
7
  rowSpan?: number;
8
8
  /** Number of columns to span (default: 1) */
9
9
  colSpan?: number;
10
+ /** Column start position (1-3, optional for explicit positioning) */
11
+ colStart?: number;
12
+ /** Row start position (1+, optional for explicit positioning) */
13
+ rowStart?: number;
10
14
  }
11
15
 
12
16
  const props = withDefaults(defineProps<Props>(), {
13
17
  rowSpan: 1,
14
18
  colSpan: 1,
15
19
  });
20
+
21
+ // Compute grid-column value based on whether explicit positioning is provided
22
+ const gridColumnValue = computed(() => {
23
+ if (props.colStart) {
24
+ return `${props.colStart} / span ${props.colSpan}`;
25
+ }
26
+ return `span ${props.colSpan}`;
27
+ });
28
+
29
+ // Compute grid-row value based on whether explicit positioning is provided
30
+ const gridRowValue = computed(() => {
31
+ if (props.rowStart) {
32
+ return `${props.rowStart} / span ${props.rowSpan}`;
33
+ }
34
+ return `span ${props.rowSpan}`;
35
+ });
16
36
  </script>
17
37
 
18
38
  <template>
@@ -20,15 +40,15 @@ const props = withDefaults(defineProps<Props>(), {
20
40
  class="ns-bento-grid-item"
21
41
  :class="[props.class]"
22
42
  :style="{
23
- gridRow: `span ${props.rowSpan}`,
24
- gridColumn: `span ${props.colSpan}`,
43
+ gridRow: gridRowValue,
44
+ gridColumn: gridColumnValue,
25
45
  }"
26
46
  >
27
47
  <slot />
28
48
  </div>
29
49
  </template>
30
50
 
31
- <style lang="scss" scoped>
51
+ <style scoped>
32
52
  /**
33
53
  * Bento Grid Item Component
34
54
  * Individual item within bento grid with span control
@@ -11,18 +11,23 @@
11
11
 
12
12
  <!-- Cover image if available -->
13
13
  <EntityImage :src="imageUrl" :alt="imageAlt || 'Card image'" />
14
+ </div>
14
15
 
15
- <!-- Content overlay at bottom -->
16
- <div class="ns-entity-card__content">
17
- <slot />
18
- </div>
16
+ <!-- Content overlay at bottom -->
17
+ <div class="ns-entity-card__content">
18
+ <h3 class="ns-entity-card__title" v-if="$slots['title']">
19
+ <slot name="title" />
20
+ </h3>
21
+ <p class="ns-entity-card__description" v-if="$slots['description']">
22
+ <slot name="description" />
23
+ </p>
19
24
  </div>
20
25
  </motion.div>
21
26
  </template>
22
27
 
23
28
  <script setup lang="ts">
24
29
  import { motion } from "motion-v";
25
- import FlickeringGridBackground from "../internal/FlickeringGridBackground.vue";
30
+ import FlickeringGridBackground from "../internal/components/FlickeringGridBackground.vue";
26
31
  import EntityImage from "./EntityImage.vue";
27
32
 
28
33
  interface Props {
@@ -36,7 +41,7 @@ interface Props {
36
41
  defineProps<Props>();
37
42
  </script>
38
43
 
39
- <style lang="scss" scoped>
44
+ <style scoped>
40
45
  /**
41
46
  * Entity Card Component
42
47
  * Glassy card with image container and text overlay
@@ -60,19 +65,19 @@ defineProps<Props>();
60
65
  /* Smooth interactions */
61
66
  transition: all 0.3s cubic-bezier(0.19, 1, 0.22, 1);
62
67
  cursor: pointer;
68
+ }
69
+
70
+ /* Hover effect */
71
+ .ns-entity-card:hover {
72
+ background: rgba(255, 255, 255, 0.12);
73
+ border-color: rgba(255, 255, 255, 0.25);
74
+ box-shadow: 0 12px 48px 0 rgba(0, 0, 0, 0.15);
75
+ transform: translateY(-4px);
76
+ }
63
77
 
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
- }
78
+ /* Active state */
79
+ .ns-entity-card:active {
80
+ transform: translateY(-2px);
76
81
  }
77
82
 
78
83
  /* ============================================ */
@@ -84,15 +89,6 @@ defineProps<Props>();
84
89
  width: 100%;
85
90
  height: 100%; /* Fill parent height (works for bento grid cells) */
86
91
  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
92
 
97
93
  /* Layout */
98
94
  display: flex;
@@ -112,15 +108,40 @@ defineProps<Props>();
112
108
  /* ============================================ */
113
109
 
114
110
  .ns-entity-card__content {
111
+ position: absolute;
115
112
  padding: var(--ns-spacing-lg);
113
+ bottom: var(--ns-spacing-sm);
114
+ left: var(--ns-spacing-sm);
115
+ right: var(--ns-spacing-sm);
116
+ z-index: 10;
116
117
 
117
118
  /* White text - same as OrganizationHero */
118
119
  color: var(--ns-color-white);
120
+ }
121
+
122
+ /* Text shadow for readability */
123
+ .ns-entity-card__content > * {
124
+ margin: 0;
125
+ text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
126
+ }
127
+
128
+ .ns-entity-card__title,
129
+ .ns-entity-card__description {
130
+ display: -webkit-box;
131
+ -webkit-box-orient: vertical;
132
+ overflow: hidden;
133
+ text-overflow: ellipsis;
134
+ }
135
+
136
+ /* Title slot - clamp to 2 lines */
137
+ .ns-entity-card__title {
138
+ line-clamp: 2;
139
+ -webkit-line-clamp: 2;
140
+ }
119
141
 
120
- /* Text shadow for readability */
121
- & > * {
122
- margin: 0;
123
- text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
124
- }
142
+ /* Description slot - clamp to 3 lines */
143
+ .ns-entity-card__description {
144
+ line-clamp: 3;
145
+ -webkit-line-clamp: 3;
125
146
  }
126
147
  </style>
@@ -32,7 +32,7 @@
32
32
 
33
33
  <script setup lang="ts">
34
34
  import { motion } from "motion-v";
35
- import FlickeringGridBackground from "../internal/FlickeringGridBackground.vue";
35
+ import FlickeringGridBackground from "../internal/components/FlickeringGridBackground.vue";
36
36
  import EntityImage from "./EntityImage.vue";
37
37
  import type { Organisation } from "../../types/entities";
38
38
 
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Bento Layout Service
3
+ * Generates responsive bento grid layouts based on item count
4
+ * Provides asymmetric patterns for visual interest with repeating patterns for scalability
5
+ */
6
+
7
+ /**
8
+ * Configuration for a single bento grid item
9
+ */
10
+ export interface BentoItemConfig {
11
+ /** Column span (1-3, default 1) */
12
+ colSpan?: number;
13
+ /** Row span (1-2, default 1) */
14
+ rowSpan?: number;
15
+ /** Column start position (1-3, optional for explicit positioning) */
16
+ colStart?: number;
17
+ /** Row start position (1+, optional for explicit positioning) */
18
+ rowStart?: number;
19
+ }
20
+
21
+ /**
22
+ * Get bento grid layout configuration based on number of items
23
+ *
24
+ * Patterns:
25
+ * - 1 item: Full width
26
+ * - 2 items: 2/3 + 1/3 for hierarchy
27
+ * - 3 items: 1 large (2x2) + 2 small stacked
28
+ * - 4 items: 1 large (2x2) + 2 small + 1 tall (1x2)
29
+ * - 5 items: 1 large (2x2) + 4 small in corners
30
+ * - 6 items: Mixed pattern (becomes repeating block)
31
+ * - 7+: Repeats 6-item pattern
32
+ *
33
+ * @param itemCount - Number of items to display in grid
34
+ * @returns Array of layout configurations for each item
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const layouts = getBentoLayout(3);
39
+ * // Returns: [
40
+ * // { colSpan: 2, rowSpan: 2 }, // Large featured
41
+ * // { colSpan: 1, rowSpan: 1 }, // Small
42
+ * // { colSpan: 1, rowSpan: 1 } // Small
43
+ * // ]
44
+ * ```
45
+ */
46
+ export function getBentoLayout(itemCount: number): BentoItemConfig[] {
47
+ if (itemCount <= 0) return [];
48
+
49
+ // Pattern for 1 item: Full width
50
+ if (itemCount === 1) {
51
+ return [{ colSpan: 3, rowSpan: 1 }];
52
+ }
53
+
54
+ // Pattern for 2 items: 2/3 width + 1/3 width (creates hierarchy)
55
+ if (itemCount === 2) {
56
+ return [
57
+ { colSpan: 2, rowSpan: 1 }, // Featured (larger)
58
+ { colSpan: 1, rowSpan: 1 }, // Secondary
59
+ ];
60
+ }
61
+
62
+ // Pattern for 3 items: 1 large (2x2) + 2 small stacked on right
63
+ if (itemCount === 3) {
64
+ return [
65
+ { colSpan: 2, rowSpan: 2 }, // Large featured (left)
66
+ { colSpan: 1, rowSpan: 1 }, // Small (top right)
67
+ { colSpan: 1, rowSpan: 1 }, // Small (bottom right)
68
+ ];
69
+ }
70
+
71
+ // Pattern for 4 items: 1 large (2x2) + 2 small + 1 wide bottom
72
+ if (itemCount === 4) {
73
+ return [
74
+ { colSpan: 2, rowSpan: 2 }, // Large featured (top left)
75
+ { colSpan: 1, rowSpan: 1 }, // Small (top right)
76
+ { colSpan: 1, rowSpan: 1 }, // Small (middle right)
77
+ { colSpan: 3, rowSpan: 1 }, // Wide (bottom, full width)
78
+ ];
79
+ }
80
+
81
+ // Pattern for 5 items: 1 large center + 4 small in corners
82
+ if (itemCount === 5) {
83
+ return [
84
+ { colSpan: 2, rowSpan: 2 }, // Large featured (top left)
85
+ { colSpan: 1, rowSpan: 1 }, // Small (top right)
86
+ { colSpan: 1, rowSpan: 1 }, // Small (middle right)
87
+ { colSpan: 1, rowSpan: 1 }, // Wide (bottom left + center)
88
+ { colSpan: 2, rowSpan: 1 }, // Small (bottom right)
89
+ ];
90
+ }
91
+
92
+ // Pattern for 6 items: First 3 + mirrored 3 (featured left, then featured right)
93
+ // Uses explicit positioning to ensure correct layout
94
+ const sixItemPattern: BentoItemConfig[] = [
95
+ { colSpan: 2, rowSpan: 2, colStart: 1, rowStart: 1 }, // Large featured (top left)
96
+ { colSpan: 1, rowSpan: 1, colStart: 3, rowStart: 1 }, // Small (top right)
97
+ { colSpan: 1, rowSpan: 1, colStart: 3, rowStart: 2 }, // Small (middle right)
98
+ { colSpan: 1, rowSpan: 1, colStart: 1, rowStart: 3 }, // Small (bottom left)
99
+ { colSpan: 1, rowSpan: 1, colStart: 1, rowStart: 4 }, // Small (far bottom left)
100
+ { colSpan: 2, rowSpan: 2, colStart: 2, rowStart: 3 }, // Large featured (bottom right)
101
+ ];
102
+
103
+ if (itemCount === 6) {
104
+ return sixItemPattern;
105
+ }
106
+
107
+ // For 7+ items: Repeat the 6-item pattern
108
+ const layouts: BentoItemConfig[] = [];
109
+ const fullPatterns = Math.floor(itemCount / 6);
110
+ const remainder = itemCount % 6;
111
+
112
+ // Add full 6-item pattern blocks
113
+ for (let i = 0; i < fullPatterns; i++) {
114
+ layouts.push(...sixItemPattern);
115
+ }
116
+
117
+ // Add partial pattern for remaining items
118
+ if (remainder > 0) {
119
+ layouts.push(...getBentoLayout(remainder));
120
+ }
121
+
122
+ return layouts;
123
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newschools/sdk",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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",