@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 +307 -138
- package/nuxt/src/runtime/components/BentoGrid.vue +5 -3
- package/nuxt/src/runtime/components/BentoGridItem.vue +23 -3
- package/nuxt/src/runtime/components/EntityCard.vue +53 -32
- package/nuxt/src/runtime/components/OrganizationHero.vue +1 -1
- package/nuxt/src/runtime/internal/services/bento-layout.service.ts +123 -0
- package/package.json +1 -1
- /package/nuxt/src/runtime/internal/{FlickeringGridBackground.vue → components/FlickeringGridBackground.vue} +0 -0
package/README.md
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
# @newschools/
|
|
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
|
-
🎯 **
|
|
11
|
-
🔑 **API key authentication** - Secure access to your
|
|
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/
|
|
18
|
+
npm install @newschools/sdk
|
|
18
19
|
|
|
19
20
|
# Using yarn
|
|
20
|
-
yarn add @newschools/
|
|
21
|
+
yarn add @newschools/sdk
|
|
21
22
|
|
|
22
23
|
# Using pnpm
|
|
23
|
-
pnpm add @newschools/
|
|
24
|
+
pnpm add @newschools/sdk
|
|
24
25
|
|
|
25
26
|
# Using bun
|
|
26
|
-
bun add @newschools/
|
|
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/
|
|
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
|
-
|
|
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/
|
|
59
|
+
modules: ["@newschools/sdk"],
|
|
52
60
|
|
|
53
61
|
newschools: {
|
|
54
|
-
apiKey:
|
|
55
|
-
|
|
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
|
-
|
|
122
|
+
### Organization Data
|
|
63
123
|
|
|
64
|
-
|
|
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
|
-
<
|
|
79
|
-
<
|
|
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
|
-
###
|
|
139
|
+
### Pre-built Components
|
|
140
|
+
|
|
141
|
+
#### OrganizationHero
|
|
142
|
+
|
|
143
|
+
Display organization header with cover image and branding:
|
|
91
144
|
|
|
92
145
|
```vue
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
102
|
-
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
###
|
|
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
|
|
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="
|
|
124
|
-
<
|
|
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
|
-
|
|
215
|
+
#### Fetch a specific journey
|
|
131
216
|
|
|
132
217
|
```vue
|
|
133
218
|
<script setup lang="ts">
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
233
|
+
#### Fetch waypoints for a journey
|
|
142
234
|
|
|
143
235
|
```vue
|
|
144
236
|
<script setup lang="ts">
|
|
145
|
-
const { data:
|
|
146
|
-
journeyId: "journey-
|
|
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
|
-
|
|
243
|
+
#### Fetch a specific waypoint
|
|
153
244
|
|
|
154
245
|
```vue
|
|
155
246
|
<script setup lang="ts">
|
|
156
|
-
const { data:
|
|
157
|
-
journeyId: "journey-
|
|
158
|
-
waypointId: "waypoint-
|
|
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/
|
|
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
|
-
|
|
213
|
-
|
|
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/
|
|
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
|
-
|
|
379
|
+
## Environment Variables
|
|
235
380
|
|
|
236
|
-
|
|
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
|
-
|
|
239
|
-
import { NewSchoolsPathResolver } from "@newschools/nuxt";
|
|
386
|
+
## Components Reference
|
|
240
387
|
|
|
241
|
-
|
|
242
|
-
// Returns: '/journeys'
|
|
388
|
+
### `<OrganizationHero>`
|
|
243
389
|
|
|
244
|
-
|
|
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
|
-
|
|
392
|
+
**Props:**
|
|
252
393
|
|
|
253
|
-
|
|
394
|
+
- `org: Organisation` - Organization object (required)
|
|
254
395
|
|
|
255
|
-
|
|
256
|
-
# In the SDK package directory
|
|
257
|
-
cd packages/nuxt
|
|
258
|
-
bun link
|
|
396
|
+
**Slots:**
|
|
259
397
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
```
|
|
398
|
+
- `sub-title` - Content below organization name
|
|
399
|
+
- `content` - Right-side content area
|
|
400
|
+
- `background` - Custom background component
|
|
264
401
|
|
|
265
|
-
|
|
402
|
+
### `<BentoGrid>`
|
|
266
403
|
|
|
267
|
-
|
|
268
|
-
export default defineNuxtConfig({
|
|
269
|
-
modules: ["@newschools/nuxt"],
|
|
270
|
-
});
|
|
271
|
-
```
|
|
404
|
+
Responsive masonry-style grid layout
|
|
272
405
|
|
|
273
|
-
|
|
406
|
+
### `<BentoGridItem>`
|
|
274
407
|
|
|
275
|
-
|
|
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
|
-
|
|
410
|
+
**Props:**
|
|
281
411
|
|
|
282
|
-
|
|
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
|
-
|
|
421
|
+
- `default` - Card content
|
|
422
|
+
- `background` - Background layer
|
|
285
423
|
|
|
286
|
-
|
|
424
|
+
### `<EntityImage>`
|
|
287
425
|
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
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:
|
|
24
|
-
gridColumn:
|
|
43
|
+
gridRow: gridRowValue,
|
|
44
|
+
gridColumn: gridColumnValue,
|
|
25
45
|
}"
|
|
26
46
|
>
|
|
27
47
|
<slot />
|
|
28
48
|
</div>
|
|
29
49
|
</template>
|
|
30
50
|
|
|
31
|
-
<style
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
File without changes
|