create-trokky 0.1.1 → 0.1.3

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,179 @@
1
+ /**
2
+ * The content model: a conference.
3
+ *
4
+ * Two singletons (event, venue) setting the site's copy, four listed collections, and the
5
+ * two many-to-many links a program is made of — sessions point at their track and their
6
+ * speakers. Type, level and tier are plain string fields with dropdown lists, so editors
7
+ * pick from a fixed set and the site can group on the value. The session document id is its
8
+ * URL identity; there are no slug fields.
9
+ */
10
+
11
+ export const eventSchema = {
12
+ name: 'event',
13
+ title: 'Event',
14
+ type: 'document',
15
+ singleton: true,
16
+ description: 'What the whole site leads with: name, tagline, dates and navigation',
17
+ fields: {
18
+ name: { type: 'string', title: 'Event name', required: true },
19
+ edition: { type: 'string', title: 'Edition', description: 'The year or edition, shown next to the name' },
20
+ tagline: { type: 'string', title: 'Tagline', description: 'One line under the name' },
21
+ description: { type: 'text', title: 'Description', options: { rows: 3 } },
22
+ startDate: { type: 'date', title: 'First day' },
23
+ endDate: { type: 'date', title: 'Last day' },
24
+ city: { type: 'string', title: 'City' },
25
+ venue: { type: 'string', title: 'Venue name', description: 'Shown in the header meta; the Venue document holds the detail' },
26
+ stats: {
27
+ type: 'array',
28
+ title: 'Stat strip',
29
+ description: 'The numbers under the hero, in order',
30
+ of: {
31
+ type: 'object',
32
+ fields: {
33
+ label: { type: 'string', title: 'Label', required: true },
34
+ value: { type: 'string', title: 'Value', required: true }
35
+ }
36
+ }
37
+ },
38
+ ctaLabel: { type: 'string', title: 'Header button label' },
39
+ ctaHref: { type: 'string', title: 'Header button path' },
40
+ navigation: {
41
+ type: 'array',
42
+ title: 'Navigation',
43
+ description: 'Links in the header, in order',
44
+ of: {
45
+ type: 'object',
46
+ fields: {
47
+ label: { type: 'string', title: 'Label', required: true },
48
+ href: { type: 'string', title: 'Path or URL', required: true }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ } as const
54
+
55
+ export const venueSchema = {
56
+ name: 'venue',
57
+ title: 'Venue',
58
+ type: 'document',
59
+ singleton: true,
60
+ description: 'Where the event is, how to get there, and what tickets cost',
61
+ fields: {
62
+ name: { type: 'string', title: 'Venue name', required: true },
63
+ address: { type: 'string', title: 'Address' },
64
+ about: { type: 'text', title: 'About the hall', options: { rows: 3 } },
65
+ travel: {
66
+ type: 'array',
67
+ title: 'Getting there',
68
+ of: {
69
+ type: 'object',
70
+ fields: {
71
+ mode: { type: 'string', title: 'Mode', required: true, description: 'Plane, metro, bike…' },
72
+ detail: { type: 'string', title: 'Detail' }
73
+ }
74
+ }
75
+ },
76
+ tickets: {
77
+ type: 'array',
78
+ title: 'Tickets',
79
+ of: {
80
+ type: 'object',
81
+ fields: {
82
+ name: { type: 'string', title: 'Name', required: true },
83
+ price: { type: 'string', title: 'Price', required: true, description: 'Shown as written, e.g. €420' },
84
+ note: { type: 'string', title: 'Note' },
85
+ features: {
86
+ type: 'array',
87
+ title: 'Features',
88
+ of: { type: 'string' }
89
+ },
90
+ featured: { type: 'boolean', title: 'Highlight', default: false }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ } as const
96
+
97
+ export const trackSchema = {
98
+ name: 'track',
99
+ title: 'Track',
100
+ type: 'document',
101
+ description: 'A room and a theme; the schedule grid uses one column per track',
102
+ fields: {
103
+ name: { type: 'string', title: 'Track name', required: true },
104
+ room: { type: 'string', title: 'Room', required: true },
105
+ color: { type: 'color', title: 'Track color', description: 'Hex color, e.g. #C8442B' },
106
+ order: { type: 'number', title: 'Order', required: true, description: 'Column position in the schedule grid, starting at 1' }
107
+ }
108
+ } as const
109
+
110
+ export const speakerSchema = {
111
+ name: 'speaker',
112
+ title: 'Speaker',
113
+ type: 'document',
114
+ description: 'A person; their sessions are resolved from the session documents',
115
+ fields: {
116
+ name: { type: 'string', title: 'Name', required: true },
117
+ role: { type: 'string', title: 'Role' },
118
+ org: { type: 'string', title: 'Organization' },
119
+ pronouns: { type: 'string', title: 'Pronouns' },
120
+ bio: { type: 'text', title: 'Bio', options: { rows: 3 } },
121
+ photo: { type: 'media', title: 'Photo', description: 'Optional; without one the site draws a monogram', options: { accept: 'image/*' } },
122
+ city: { type: 'string', title: 'City' }
123
+ }
124
+ } as const
125
+
126
+ export const sessionSchema = {
127
+ name: 'session',
128
+ title: 'Session',
129
+ type: 'document',
130
+ description: 'One slot in the program; its id is its URL',
131
+ fields: {
132
+ title: { type: 'string', title: 'Title', required: true },
133
+ type: {
134
+ type: 'string',
135
+ title: 'Type',
136
+ required: true,
137
+ options: { list: ['Keynote', 'Talk', 'Workshop', 'Panel'] }
138
+ },
139
+ day: { type: 'date', title: 'Day', required: true },
140
+ start: { type: 'string', title: 'Starts at', required: true, description: 'HH:MM', validation: { maxLength: 5 } },
141
+ end: { type: 'string', title: 'Ends at', required: true, description: 'HH:MM', validation: { maxLength: 5 } },
142
+ track: { type: 'reference', title: 'Track', to: 'track', required: true },
143
+ speakers: {
144
+ type: 'array',
145
+ title: 'Speakers',
146
+ description: 'Who is in this session',
147
+ of: { type: 'reference', to: 'speaker' }
148
+ },
149
+ abstract: { type: 'text', title: 'Abstract', options: { rows: 3 } },
150
+ tags: { type: 'array', title: 'Tags', of: { type: 'string' } },
151
+ level: {
152
+ type: 'string',
153
+ title: 'Level',
154
+ options: { list: ['All', 'Intro', 'Intermediate', 'Advanced'] }
155
+ }
156
+ }
157
+ } as const
158
+
159
+ export const sponsorSchema = {
160
+ name: 'sponsor',
161
+ title: 'Sponsor',
162
+ type: 'document',
163
+ description: 'A sponsor; the site groups them by tier, in that order',
164
+ fields: {
165
+ name: { type: 'string', title: 'Name', required: true },
166
+ tier: {
167
+ type: 'string',
168
+ title: 'Tier',
169
+ required: true,
170
+ options: { list: ['platinum', 'gold', 'silver', 'partner'] },
171
+ description: 'Lowercase; tiers are grouped in platinum, gold, silver, partner order'
172
+ },
173
+ blurb: { type: 'text', title: 'Blurb', options: { rows: 2 } },
174
+ url: { type: 'url', title: 'Website' },
175
+ logo: { type: 'media', title: 'Logo', description: 'Optional; without one the site draws a wordmark plate', options: { accept: 'image/*' } }
176
+ }
177
+ } as const
178
+
179
+ export const schemas = [eventSchema, venueSchema, trackSchema, speakerSchema, sessionSchema, sponsorSchema]
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Sample content, created the moment the instance is claimed.
3
+ *
4
+ * An empty CMS shows an empty site, and the first minute decides whether a newcomer keeps
5
+ * going. So the new owner’s first sight of both the site and the Studio is a whole working
6
+ * conference — six tracks, eight speakers, a fourteen-slot program, sponsors, a venue with
7
+ * tickets — that they can edit or delete. Every document id is fixed, so seeding twice is
8
+ * harmless; sessions are only created when there are none. This template ships no media:
9
+ * speakers and sponsors render generated monograms until real uploads replace them.
10
+ */
11
+ import type { TrokkyCore } from '@trokky/trokky'
12
+
13
+ /** Read one file from `public/seed/`. The emitted hook always passes a reader; here it goes
14
+ * unused, because this template’s seeds are text documents only. */
15
+ export type SeedAssetReader = (path: string) => Promise<ArrayBuffer>
16
+
17
+ const P = { _status: 'published' } as const
18
+
19
+ export async function seedSampleContent(core: TrokkyCore, readAsset: SeedAssetReader): Promise<void> {
20
+ if ((await core.listDocuments('session', { limit: 1 })).length > 0) return
21
+
22
+ await core.saveDocument('event', {
23
+ id: 'event', ...P,
24
+ name: 'Frameworks Live',
25
+ edition: '2026',
26
+ tagline: 'A field guide to building for the next decade of the web.',
27
+ description: 'Three days, six tracks, fourteen sessions and eight speakers. Frameworks Live is the annual gathering for the people who build the platforms other developers build on — runtimes, editors, headless CMSs, and the frameworks that tie them together.',
28
+ startDate: '2026-05-13',
29
+ endDate: '2026-05-15',
30
+ city: 'Lisbon',
31
+ venue: 'Cordoaria Nacional',
32
+ stats: [
33
+ { label: 'Days', value: '3' },
34
+ { label: 'Tracks', value: '6' },
35
+ { label: 'Sessions', value: '14' },
36
+ { label: 'Speakers', value: '8' }
37
+ ],
38
+ ctaLabel: 'Get your pass',
39
+ ctaHref: '/venue#tickets',
40
+ navigation: [
41
+ { label: 'Schedule', href: '/schedule' },
42
+ { label: 'Speakers', href: '/speakers' },
43
+ { label: 'Sessions', href: '/sessions' },
44
+ { label: 'Sponsors', href: '/sponsors' },
45
+ { label: 'Venue', href: '/venue' }
46
+ ]
47
+ })
48
+
49
+ await core.saveDocument('venue', {
50
+ id: 'venue', ...P,
51
+ name: 'Cordoaria Nacional',
52
+ address: 'Av. da Índia, 2050-014 Lisboa, Portugal',
53
+ about: 'A former 18th-century rope factory on the Lisbon waterfront, now a landmark hall for the city’s biggest gatherings. Forty-foot ceilings, exposed brick, and the Tagus river at the door.',
54
+ travel: [
55
+ { mode: 'Plane', detail: 'Lisbon Airport (LIS) is 7km / 20 min by metro. Direct shuttles run every 20 minutes during conference days.' },
56
+ { mode: 'Metro', detail: 'Take the Linha Verde (green) to Cais do Sodré, then a 10-minute riverside walk west along Av. da Índia.' },
57
+ { mode: 'Bike', detail: 'Secure bike parking on the riverside promenade. There are 40 racks under the western canopy.' },
58
+ { mode: 'Car', detail: 'Limited paid parking on-site (€6/day). We strongly recommend transit — the riverside approach is walk-only during event hours.' }
59
+ ],
60
+ tickets: [
61
+ {
62
+ name: 'Conference',
63
+ price: '€420',
64
+ note: 'All three days, all tracks, recordings included.',
65
+ features: ['3-day full access', '14 sessions, 6 tracks', 'Recordings within 48h', 'Welcome reception'],
66
+ featured: false
67
+ },
68
+ {
69
+ name: 'Workshop',
70
+ price: '€180',
71
+ note: 'Single workshop seat, limited capacity.',
72
+ features: ['One 90-min workshop', 'Same-day track access', 'Lunch'],
73
+ featured: false
74
+ },
75
+ {
76
+ name: 'Community',
77
+ price: '€0',
78
+ note: 'For maintainers of qualifying open projects.',
79
+ features: ['Full access, sponsored seats', 'Apply with your project', 'Travel fund available'],
80
+ featured: true
81
+ }
82
+ ]
83
+ })
84
+
85
+ await core.saveDocument('track', { id: 'track-01', ...P, name: 'Runtimes', room: 'Hall A', color: '#C8442B', order: 1 })
86
+ await core.saveDocument('track', { id: 'track-02', ...P, name: 'Editors & DX', room: 'Hall B', color: '#2C6E6A', order: 2 })
87
+ await core.saveDocument('track', { id: 'track-03', ...P, name: 'Headless & Content', room: 'Hall C', color: '#B8860B', order: 3 })
88
+ await core.saveDocument('track', { id: 'track-04', ...P, name: 'Frameworks', room: 'Hall D', color: '#5B4B8A', order: 4 })
89
+ await core.saveDocument('track', { id: 'track-05', ...P, name: 'Edge & Infra', room: 'Hall E', color: '#3D6B9E', order: 5 })
90
+ await core.saveDocument('track', { id: 'track-06', ...P, name: 'Community', room: 'Hall F', color: '#8A3324', order: 6 })
91
+
92
+ await core.saveDocument('speaker', {
93
+ id: 'speaker-01', ...P,
94
+ name: 'Nadia Eke', role: 'Runtime engineer', org: 'Meridian Runtime', pronouns: 'she/her',
95
+ bio: 'Nadia works on the engine’s optimizer pipeline and has shipped the fast-calls path used by every modern serverless runtime. Before that she built JIT profilers at a trading firm where milliseconds were measured in dollars.',
96
+ city: 'Munich'
97
+ })
98
+ await core.saveDocument('speaker', {
99
+ id: 'speaker-02', ...P,
100
+ name: 'Tomás Ribeiro', role: 'Editor architect', org: 'Quillworks', pronouns: 'he/him',
101
+ bio: 'Tomás leads the rendering layer of a popular collaborative editor. He thinks a lot about CRDTs, latency budgets, and why your editor’s cursor jump is the most intimate thing in software.',
102
+ city: 'Lisbon'
103
+ })
104
+ await core.saveDocument('speaker', {
105
+ id: 'speaker-03', ...P,
106
+ name: 'Priya Nair', role: 'Founder', org: 'Blockhead CMS', pronouns: 'she/they',
107
+ bio: 'Priya built and open-sourced a headless CMS now used by two thousand studios. She writes about structured content, schema-first design, and why your blog should not be a database table.',
108
+ city: 'Bangalore'
109
+ })
110
+ await core.saveDocument('speaker', {
111
+ id: 'speaker-04', ...P,
112
+ name: 'Marcus Feld', role: 'Framework maintainer', org: 'Lumen UI', pronouns: 'he/him',
113
+ bio: 'Marcus is a core maintainer on a fine-grained reactive framework and has been writing about signals long before they were cool. He will not let you call them "reactive state".',
114
+ city: 'Berlin'
115
+ })
116
+ await core.saveDocument('speaker', {
117
+ id: 'speaker-05', ...P,
118
+ name: 'Lucia Moreau', role: 'Edge platform lead', org: 'Kestrel Compute', pronouns: 'she/her',
119
+ bio: 'Lucia runs the runtime-at-the-edge team at a global compute platform. She has opinions about cold starts that she will share whether or not you ask.',
120
+ city: 'Paris'
121
+ })
122
+ await core.saveDocument('speaker', {
123
+ id: 'speaker-06', ...P,
124
+ name: 'Devon Park', role: 'DX advocate', org: 'Northship', pronouns: 'they/them',
125
+ bio: 'Devon writes the docs that get screenshot-ed into conference talks. They care about error messages, onboarding flows, and the shape of the first ten minutes of a tool.',
126
+ city: 'New York'
127
+ })
128
+ await core.saveDocument('speaker', {
129
+ id: 'speaker-07', ...P,
130
+ name: 'Hana Sato', role: 'Tooling engineer', org: 'Furlong', pronouns: 'she/her',
131
+ bio: 'Hana works on bundler internals and has been quietly deleting milliseconds from the dev-server startup path for four years.',
132
+ city: 'Tokyo'
133
+ })
134
+ await core.saveDocument('speaker', {
135
+ id: 'speaker-08', ...P,
136
+ name: 'Idris Bello', role: 'Community lead', org: 'Commons Fund', pronouns: 'he/him',
137
+ bio: 'Idris helps maintainers get paid. He has read more maintainer burnout post-mortems than anyone and will tell you what they have in common.',
138
+ city: 'Lagos'
139
+ })
140
+
141
+ await core.saveDocument('session', {
142
+ id: 'session-01', ...P,
143
+ title: 'Fast-calls: stealing 40µs from every function call',
144
+ type: 'Talk', day: '2026-05-13', start: '09:30', end: '10:15',
145
+ track: 'track-01', speakers: ['speaker-01'],
146
+ abstract: 'A deep dive into the inline-cache path that lets modern runtimes skip argument count checks. We’ll look at the bytecode, the assembly the engine actually emits, and the one flag you can flip in your own runtime to inherit the speedup.',
147
+ tags: ['runtime', 'performance', 'jit'],
148
+ level: 'Advanced'
149
+ })
150
+ await core.saveDocument('session', {
151
+ id: 'session-02', ...P,
152
+ title: 'The cursor is the interface',
153
+ type: 'Talk', day: '2026-05-13', start: '10:30', end: '11:15',
154
+ track: 'track-02', speakers: ['speaker-02'],
155
+ abstract: 'Why does a remote cursor jump the wrong way? A tour of CRDT selection-state, the difference between presence and intent, and how to make a collaborative editor feel local at 300ms round-trip.',
156
+ tags: ['crdt', 'editors', 'realtime'],
157
+ level: 'Intermediate'
158
+ })
159
+ await core.saveDocument('session', {
160
+ id: 'session-03', ...P,
161
+ title: 'Your content model is your API',
162
+ type: 'Keynote', day: '2026-05-13', start: '11:30', end: '12:30',
163
+ track: 'track-03', speakers: ['speaker-03'],
164
+ abstract: 'A keynote on why a headless CMS is really a typed query language, what a good content schema gets you for free, and the design decisions behind an open one.',
165
+ tags: ['cms', 'structured-content', 'open-source'],
166
+ level: 'All'
167
+ })
168
+ await core.saveDocument('session', {
169
+ id: 'session-04', ...P,
170
+ title: 'Signals without the magic',
171
+ type: 'Talk', day: '2026-05-13', start: '14:00', end: '14:45',
172
+ track: 'track-04', speakers: ['speaker-04'],
173
+ abstract: 'Fine-grained reactivity from first principles: a 60-line reimplementation of a signal store, and where the real frameworks spend the rest of their lines.',
174
+ tags: ['frameworks', 'reactivity', 'ui'],
175
+ level: 'Intermediate'
176
+ })
177
+ await core.saveDocument('session', {
178
+ id: 'session-05', ...P,
179
+ title: 'Cold starts are a design problem',
180
+ type: 'Talk', day: '2026-05-13', start: '15:00', end: '15:45',
181
+ track: 'track-05', speakers: ['speaker-05'],
182
+ abstract: 'Snapshot restore, lazy module init, and the trade-off between image size and first-request latency. Numbers from a platform that serves a billion functions a day.',
183
+ tags: ['edge', 'serverless', 'performance'],
184
+ level: 'Advanced'
185
+ })
186
+ await core.saveDocument('session', {
187
+ id: 'session-06', ...P,
188
+ title: 'The first ten minutes',
189
+ type: 'Talk', day: '2026-05-13', start: '16:00', end: '16:45',
190
+ track: 'track-02', speakers: ['speaker-06'],
191
+ abstract: 'A practical framework for auditing a developer’s first ten minutes with your tool. Bring your own README.',
192
+ tags: ['dx', 'docs', 'onboarding'],
193
+ level: 'All'
194
+ })
195
+ await core.saveDocument('session', {
196
+ id: 'session-07', ...P,
197
+ title: 'Workshop: build a multiplayer editor in 90 minutes',
198
+ type: 'Workshop', day: '2026-05-14', start: '09:30', end: '11:00',
199
+ track: 'track-02', speakers: ['speaker-02'],
200
+ abstract: 'Hands-on: from a textarea to a CRDT-backed collaborative editor with presence, cursors, and undo. Laptop required; we’ll provide the starter repo.',
201
+ tags: ['crdt', 'workshop', 'editors'],
202
+ level: 'Intermediate'
203
+ })
204
+ await core.saveDocument('session', {
205
+ id: 'session-08', ...P,
206
+ title: 'Deleting the dev-server restart',
207
+ type: 'Talk', day: '2026-05-14', start: '11:15', end: '12:00',
208
+ track: 'track-04', speakers: ['speaker-07'],
209
+ abstract: 'How a dev server can keep module graphs hot across file moves and renames, and the dirty module-cache tricks that make it feel instant.',
210
+ tags: ['tooling', 'dx', 'bundler'],
211
+ level: 'Advanced'
212
+ })
213
+ await core.saveDocument('session', {
214
+ id: 'session-09', ...P,
215
+ title: 'Panel: what is a runtime, now?',
216
+ type: 'Panel', day: '2026-05-14', start: '14:00', end: '15:00',
217
+ track: 'track-01', speakers: ['speaker-01', 'speaker-05'],
218
+ abstract: 'The line between a runtime, a framework, and a platform has dissolved. Where do the people who build them draw it now?',
219
+ tags: ['runtime', 'panel', 'platforms'],
220
+ level: 'All'
221
+ })
222
+ await core.saveDocument('session', {
223
+ id: 'session-10', ...P,
224
+ title: 'Paying maintainers without burning them out',
225
+ type: 'Talk', day: '2026-05-14', start: '15:15', end: '16:00',
226
+ track: 'track-06', speakers: ['speaker-08'],
227
+ abstract: 'What three years of maintainer-funding data says about which models actually work, and the one variable that predicts burnout better than hours.',
228
+ tags: ['community', 'open-source', 'funding'],
229
+ level: 'All'
230
+ })
231
+ await core.saveDocument('session', {
232
+ id: 'session-11', ...P,
233
+ title: 'Workshop: ship a function to the edge',
234
+ type: 'Workshop', day: '2026-05-14', start: '16:15', end: '17:45',
235
+ track: 'track-05', speakers: ['speaker-05'],
236
+ abstract: 'From local handler to a globally-distributed function with snapshot cold-starts. We’ll measure latency from five regions.',
237
+ tags: ['edge', 'workshop', 'serverless'],
238
+ level: 'Intermediate'
239
+ })
240
+ await core.saveDocument('session', {
241
+ id: 'session-12', ...P,
242
+ title: 'Keynote: the framework is the compiler',
243
+ type: 'Keynote', day: '2026-05-15', start: '09:30', end: '10:30',
244
+ track: 'track-04', speakers: ['speaker-04'],
245
+ abstract: 'UI frameworks are quietly becoming compilers. A look at what we gain when the framework can see your whole component tree at build time.',
246
+ tags: ['frameworks', 'compilers', 'ui'],
247
+ level: 'All'
248
+ })
249
+ await core.saveDocument('session', {
250
+ id: 'session-13', ...P,
251
+ title: 'Bundling for the next decade',
252
+ type: 'Talk', day: '2026-05-15', start: '11:00', end: '11:45',
253
+ track: 'track-04', speakers: ['speaker-07'],
254
+ abstract: 'Native ESM, import attributes, and what a bundler even needs to do when browsers ship most of it natively. Some surprising answers.',
255
+ tags: ['bundler', 'modules', 'tooling'],
256
+ level: 'Intermediate'
257
+ })
258
+ await core.saveDocument('session', {
259
+ id: 'session-14', ...P,
260
+ title: 'Closing: the people who stay',
261
+ type: 'Keynote', day: '2026-05-15', start: '16:00', end: '17:00',
262
+ track: 'track-06', speakers: ['speaker-08'],
263
+ abstract: 'A closing reflection on the people who maintain the invisible platforms the rest of us build on, and what the next decade asks of them.',
264
+ tags: ['community', 'open-source', 'keynote'],
265
+ level: 'All'
266
+ })
267
+
268
+ await core.saveDocument('sponsor', { id: 'sponsor-01', ...P, name: 'Blockhead', tier: 'platinum', blurb: 'The open headless CMS. Hosted or self, structured content for everyone.', url: '#' })
269
+ await core.saveDocument('sponsor', { id: 'sponsor-02', ...P, name: 'Kestrel Compute', tier: 'platinum', blurb: 'Run full apps and databases close to your users, anywhere.', url: '#' })
270
+ await core.saveDocument('sponsor', { id: 'sponsor-03', ...P, name: 'Northship', tier: 'gold', blurb: 'The platform for frontend developers.', url: '#' })
271
+ await core.saveDocument('sponsor', { id: 'sponsor-04', ...P, name: 'Quillworks', tier: 'gold', blurb: 'The editor built for collaboration at the speed of thought.', url: '#' })
272
+ await core.saveDocument('sponsor', { id: 'sponsor-05', ...P, name: 'Furlong', tier: 'gold', blurb: 'Next generation frontend tooling.', url: '#' })
273
+ await core.saveDocument('sponsor', { id: 'sponsor-06', ...P, name: 'Lumen UI', tier: 'silver', blurb: 'Simple performant reactive UI.', url: '#' })
274
+ await core.saveDocument('sponsor', { id: 'sponsor-07', ...P, name: 'Commons Fund', tier: 'silver', blurb: 'Funding for open source communities.', url: '#' })
275
+ await core.saveDocument('sponsor', { id: 'sponsor-08', ...P, name: 'Cordoaria Press', tier: 'partner', blurb: 'Independent publisher of the proceedings.', url: '#' })
276
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * How content is arranged in the Studio sidebar.
3
+ *
4
+ * The schema decides what a singleton is; this only decides where things appear and which
5
+ * document id the singleton uses.
6
+ */
7
+ export const structure = {
8
+ title: 'Conference',
9
+ items: [
10
+ { type: 'documentList', title: 'Sessions', schemaType: 'session', icon: 'presentation-chart-line' },
11
+ { type: 'documentList', title: 'Speakers', schemaType: 'speaker', icon: 'user-group' },
12
+ { type: 'documentList', title: 'Tracks', schemaType: 'track', icon: 'squares-2x2' },
13
+ { type: 'documentList', title: 'Sponsors', schemaType: 'sponsor', icon: 'star' },
14
+ { type: 'divider' },
15
+ { type: 'singleton', title: 'Event', schemaType: 'event', documentId: 'event', icon: 'sparkle' },
16
+ { type: 'singleton', title: 'Venue', schemaType: 'venue', documentId: 'venue', icon: 'map-pin' }
17
+ ]
18
+ }
@@ -0,0 +1,27 @@
1
+ ---
2
+ /**
3
+ * A speaker's portrait block. With an uploaded photo it renders that; without one it draws the
4
+ * deterministic monogram — a duotone block in a color seeded from the speaker's id — so the
5
+ * template ships no image assets at all.
6
+ */
7
+ import { mediaUrl, colorFor, initials } from '../trokky/site'
8
+ interface Props { speaker: Record<string, any>; size?: number }
9
+ const { speaker, size = 220 } = Astro.props
10
+
11
+ const photo = mediaUrl(speaker.photo, 'thumbnail')
12
+ const bg = colorFor(String(speaker.id))
13
+ const txt = '#FAF7F2'
14
+ const init = initials(String(speaker.name))
15
+ const svgSize = Math.round(size)
16
+ ---
17
+ {photo ? (
18
+ <img class="avatar" src={photo} alt={speaker.name} style={`width:${size}px;height:${size}px`} />
19
+ ) : (
20
+ <span class="avatar" role="img" aria-label={speaker.name} style={`width:${size}px;height:${size}px;background:${bg};color:${txt}`}>
21
+ <svg viewBox={`0 0 ${svgSize} ${svgSize}`} width={svgSize} height={svgSize} xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
22
+ <rect width={svgSize} height={svgSize} fill={bg} />
23
+ <rect x="0" y={svgSize * 0.72} width={svgSize} height={svgSize * 0.28} fill="#000" opacity="0.18" />
24
+ <text x="50%" y="50%" dy="0.06em" text-anchor="middle" font-family="Fraunces, Georgia, serif" font-weight="500" font-size={Math.round(svgSize * 0.34)} fill={txt}>{init}</text>
25
+ </svg>
26
+ </span>
27
+ )}
@@ -0,0 +1,27 @@
1
+ ---
2
+ /**
3
+ * A session row. In list form the meta shows the speakers, then the track; with `showDay` it
4
+ * shows the day and the track instead, for speaker pages where the speakers are already known.
5
+ */
6
+ import { formatDate } from '../trokky/site'
7
+ interface Props { session: Record<string, any>; showDay?: boolean }
8
+ const { session, showDay = false } = Astro.props
9
+ const s = session
10
+ ---
11
+ <a class="session-row" href={`/sessions/${s.id}`}>
12
+ <span class="session-time">
13
+ <span class="t-start">{s.start}</span>
14
+ <span class="t-end">{s.end}</span>
15
+ </span>
16
+ <span class="session-pill" style={`--c: ${s.track?.color || '#333'}`}>{s.type}</span>
17
+ <span class="session-main">
18
+ <span class="session-title">{s.title}</span>
19
+ <span class="session-meta">
20
+ {showDay
21
+ ? <>{formatDate(s.day)}{s.track ? <> · <span class="session-track">{s.track.name}</span></> : null}</>
22
+ : s.speakers.map((sp: any) => <span class="session-speaker">{sp.name}</span>)}
23
+ {!showDay && s.track ? <> · <span class="session-track">{s.track.name}</span></> : null}
24
+ </span>
25
+ </span>
26
+ <span class="session-arrow" aria-hidden="true">→</span>
27
+ </a>
@@ -0,0 +1,12 @@
1
+ ---
2
+ import Avatar from './Avatar.astro'
3
+ interface Props { speaker: Record<string, any>; size?: number }
4
+ const { speaker, size = 220 } = Astro.props
5
+ ---
6
+ <a class="card speaker-card" href={`/speakers/${speaker.id}`}>
7
+ <Avatar speaker={speaker} size={size} />
8
+ <div class="card-body">
9
+ <h3 class="card-name">{speaker.name}</h3>
10
+ <p class="card-role">{[speaker.role, speaker.org].filter(Boolean).join(' · ')}</p>
11
+ </div>
12
+ </a>
@@ -0,0 +1,16 @@
1
+ ---
2
+ /**
3
+ * A sponsor's mark. With an uploaded logo it renders that; without one it draws the wordmark
4
+ * plate in a color seeded from the sponsor's name.
5
+ */
6
+ import { mediaUrl, colorFor } from '../trokky/site'
7
+ interface Props { sponsor: Record<string, any> }
8
+ const { sponsor } = Astro.props
9
+
10
+ const logo = mediaUrl(sponsor.logo, 'thumbnail')
11
+ ---
12
+ {logo ? (
13
+ <img class="sponsor-mark-img" src={logo} alt={sponsor.name} />
14
+ ) : (
15
+ <span class="sponsor-mark" style={`--mark: ${colorFor(String(sponsor.name))}`}>{sponsor.name}</span>
16
+ )}