@axium/calendar 0.1.4 → 0.2.0

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/db.json CHANGED
@@ -67,6 +67,21 @@
67
67
  "attendees:eventId",
68
68
  "attendees:userId"
69
69
  ]
70
+ },
71
+ {
72
+ "delta": true,
73
+ "alter_tables": {
74
+ "calendars": {
75
+ "add_columns": {
76
+ "color": { "type": "integer" }
77
+ }
78
+ },
79
+ "events": {
80
+ "add_columns": {
81
+ "color": { "type": "integer" }
82
+ }
83
+ }
84
+ }
70
85
  }
71
86
  ],
72
87
  "wipe": ["calendars", "events", "acl.calendars"],
package/dist/common.d.ts CHANGED
@@ -54,6 +54,7 @@ export declare const EventData: z.ZodObject<{
54
54
  end: z.ZodCoercedDate<unknown>;
55
55
  isAllDay: z.ZodCoercedBoolean<unknown>;
56
56
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
57
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
57
58
  recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
58
59
  recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
59
60
  recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -68,6 +69,7 @@ export declare const EventInit: z.ZodObject<{
68
69
  end: z.ZodCoercedDate<unknown>;
69
70
  isAllDay: z.ZodCoercedBoolean<unknown>;
70
71
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
72
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
71
73
  recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
72
74
  recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
73
75
  recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -89,6 +91,7 @@ export declare const Event: z.ZodObject<{
89
91
  end: z.ZodCoercedDate<unknown>;
90
92
  isAllDay: z.ZodCoercedBoolean<unknown>;
91
93
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
94
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
92
95
  recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
93
96
  recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
94
97
  recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -117,6 +120,7 @@ export declare const Calendar: z.ZodObject<{
117
120
  id: z.ZodUUID;
118
121
  userId: z.ZodUUID;
119
122
  created: z.ZodCoercedDate<unknown>;
123
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
120
124
  acl: z.ZodOptional<z.ZodArray<z.ZodObject<{
121
125
  itemId: z.ZodUUID;
122
126
  userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -157,6 +161,7 @@ declare const CalendarAPI: {
157
161
  id: z.ZodUUID;
158
162
  userId: z.ZodUUID;
159
163
  created: z.ZodCoercedDate<unknown>;
164
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
160
165
  acl: z.ZodOptional<z.ZodArray<z.ZodObject<{
161
166
  itemId: z.ZodUUID;
162
167
  userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -185,6 +190,7 @@ declare const CalendarAPI: {
185
190
  id: z.ZodUUID;
186
191
  userId: z.ZodUUID;
187
192
  created: z.ZodCoercedDate<unknown>;
193
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
188
194
  acl: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
189
195
  itemId: z.ZodUUID;
190
196
  userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -215,6 +221,7 @@ declare const CalendarAPI: {
215
221
  id: z.ZodUUID;
216
222
  userId: z.ZodUUID;
217
223
  created: z.ZodCoercedDate<unknown>;
224
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
218
225
  acl: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
219
226
  itemId: z.ZodUUID;
220
227
  userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -245,6 +252,7 @@ declare const CalendarAPI: {
245
252
  id: z.ZodUUID;
246
253
  userId: z.ZodUUID;
247
254
  created: z.ZodCoercedDate<unknown>;
255
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
248
256
  acl: z.ZodOptional<z.ZodArray<z.ZodObject<{
249
257
  itemId: z.ZodUUID;
250
258
  userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -273,6 +281,7 @@ declare const CalendarAPI: {
273
281
  id: z.ZodUUID;
274
282
  userId: z.ZodUUID;
275
283
  created: z.ZodCoercedDate<unknown>;
284
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
276
285
  acl: z.ZodOptional<z.ZodArray<z.ZodObject<{
277
286
  itemId: z.ZodUUID;
278
287
  userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -309,6 +318,7 @@ declare const CalendarAPI: {
309
318
  end: z.ZodCoercedDate<unknown>;
310
319
  isAllDay: z.ZodCoercedBoolean<unknown>;
311
320
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
321
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
312
322
  recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
313
323
  recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
314
324
  recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -331,6 +341,7 @@ declare const CalendarAPI: {
331
341
  end: z.ZodCoercedDate<unknown>;
332
342
  isAllDay: z.ZodCoercedBoolean<unknown>;
333
343
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
344
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
334
345
  recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
335
346
  recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
336
347
  recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -349,6 +360,7 @@ declare const CalendarAPI: {
349
360
  end: z.ZodCoercedDate<unknown>;
350
361
  isAllDay: z.ZodCoercedBoolean<unknown>;
351
362
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
363
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
352
364
  recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
353
365
  recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
354
366
  recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -373,6 +385,7 @@ declare const CalendarAPI: {
373
385
  end: z.ZodCoercedDate<unknown>;
374
386
  isAllDay: z.ZodCoercedBoolean<unknown>;
375
387
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
388
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
376
389
  recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
377
390
  recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
378
391
  recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -395,6 +408,7 @@ declare const CalendarAPI: {
395
408
  end: z.ZodCoercedDate<unknown>;
396
409
  isAllDay: z.ZodCoercedBoolean<unknown>;
397
410
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
411
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
398
412
  recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
399
413
  recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
400
414
  recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -413,6 +427,7 @@ declare const CalendarAPI: {
413
427
  end: z.ZodCoercedDate<unknown>;
414
428
  isAllDay: z.ZodCoercedBoolean<unknown>;
415
429
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
430
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
416
431
  recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
417
432
  recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
418
433
  recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -435,6 +450,7 @@ declare const CalendarAPI: {
435
450
  end: z.ZodCoercedDate<unknown>;
436
451
  isAllDay: z.ZodCoercedBoolean<unknown>;
437
452
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
453
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
438
454
  recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
439
455
  recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
440
456
  recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
package/dist/common.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { $API, AccessControl, pickPermissions } from '@axium/core';
2
2
  import * as z from 'zod';
3
3
  import $pkg from '../package.json' with { type: 'json' };
4
+ import { Color } from '@axium/core/color';
4
5
  export function dayOfYear(date) {
5
6
  const yearStart = new Date(date.getFullYear(), 0, 1);
6
7
  return Math.round((date.getTime() - yearStart.getTime()) / 86400000 + 1);
@@ -81,6 +82,7 @@ export const EventData = z.object({
81
82
  end: z.coerce.date(),
82
83
  isAllDay: z.coerce.boolean(),
83
84
  description: z.string().max(2000).nullish(),
85
+ color: Color.nullish(),
84
86
  // note: recurrences are not support yet
85
87
  recurrence: z.string().max(1000).nullish(),
86
88
  recurrenceExcludes: z.string().max(100).array().max(100).nullish(),
@@ -111,6 +113,7 @@ export const Calendar = CalendarInit.extend({
111
113
  id: z.uuid(),
112
114
  userId: z.uuid(),
113
115
  created: z.coerce.date(),
116
+ color: Color.nullish(),
114
117
  acl: AccessControl.array().optional(),
115
118
  });
116
119
  export function getCalPermissionsInfo(cal, user) {
package/dist/server.js CHANGED
@@ -7,28 +7,37 @@ import { sql } from 'kysely';
7
7
  import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
8
8
  import * as z from 'zod';
9
9
  import { Attendee, CalendarInit, EventFilter, EventInit } from './common.js';
10
+ function withEncoded(value) {
11
+ return (value.color === undefined || value.color === null
12
+ ? value
13
+ : Object.assign(value, { color: value.color.toString(16).padStart(8, '0') }));
14
+ }
15
+ function withDecoded(value) {
16
+ return (value.color === undefined || value.color === null ? value : Object.assign(value, { color: parseInt(value.color, 16) }));
17
+ }
10
18
  addRoute({
11
19
  path: '/api/users/:id/calendars',
12
20
  params: { id: z.uuid() },
13
21
  async GET(request, { id: userId }) {
14
22
  const { user } = await checkAuthForUser(request, userId);
15
- return await database
23
+ const result = await database
16
24
  .selectFrom('calendars')
17
25
  .selectAll()
18
26
  .select(aclFrom('calendars'))
19
27
  .where(userHasAccess('calendars', user))
20
28
  .execute()
21
29
  .catch(withError('Could not get calendars'));
30
+ return result.map(c => withEncoded(c));
22
31
  },
23
32
  async PUT(request, { id: userId }) {
24
33
  const init = await parseBody(request, CalendarInit);
25
34
  await checkAuthForUser(request, userId);
26
- return await database
35
+ return withEncoded(await database
27
36
  .insertInto('calendars')
28
37
  .values({ ...init, userId })
29
38
  .returningAll()
30
39
  .executeTakeFirstOrThrow()
31
- .catch(withError('Could not create calendar'));
40
+ .catch(withError('Could not create calendar')));
32
41
  },
33
42
  });
34
43
  addRoute({
@@ -36,27 +45,27 @@ addRoute({
36
45
  params: { id: z.uuid() },
37
46
  async GET(request, { id }) {
38
47
  const { item } = await authRequestForItem(request, 'calendars', id, { read: true });
39
- return item;
48
+ return withEncoded(item);
40
49
  },
41
50
  async PATCH(request, { id }) {
42
51
  const body = await parseBody(request, CalendarInit);
43
52
  await authRequestForItem(request, 'calendars', id, { edit: true });
44
- return await database
53
+ return withEncoded(await database
45
54
  .updateTable('calendars')
46
55
  .set(body)
47
56
  .where('id', '=', id)
48
57
  .returningAll()
49
58
  .executeTakeFirstOrThrow()
50
- .catch(withError('Could not update calendar'));
59
+ .catch(withError('Could not update calendar')));
51
60
  },
52
61
  async DELETE(request, { id }) {
53
62
  await authRequestForItem(request, 'calendars', id, { manage: true });
54
- return await database
63
+ return withEncoded(await database
55
64
  .deleteFrom('calendars')
56
65
  .where('id', '=', id)
57
66
  .returningAll()
58
67
  .executeTakeFirstOrThrow()
59
- .catch(withError('Could not delete calendar'));
68
+ .catch(withError('Could not delete calendar')));
60
69
  },
61
70
  });
62
71
  function withAttendees(eb) {
@@ -81,9 +90,10 @@ addRoute({
81
90
  .where(sql `(${sql.ref('start')}, ${sql.ref('end')}) OVERLAPS (${sql.val(filter.start)}, ${sql.val(filter.end)})`)
82
91
  .limit(1000)
83
92
  .execute()
93
+ .then(result => result.map(withEncoded))
84
94
  .catch(withError('Could not get events'));
85
95
  for (const event of events)
86
- event.calendar = calendar;
96
+ event.calendar = withEncoded(calendar);
87
97
  return events;
88
98
  },
89
99
  async PUT(request, { id }) {
@@ -93,7 +103,7 @@ addRoute({
93
103
  try {
94
104
  const event = await tx
95
105
  .insertInto('events')
96
- .values({ ...init, calId: id })
106
+ .values({ ...withDecoded(init), calId: id })
97
107
  .returningAll()
98
108
  .executeTakeFirstOrThrow()
99
109
  .catch(withError('Could not create event'));
@@ -105,7 +115,11 @@ addRoute({
105
115
  .execute()
106
116
  : []);
107
117
  await tx.commit().execute();
108
- return Object.assign(event, { attendees, calendar });
118
+ return Object.assign(event, {
119
+ attendees,
120
+ calendar: withEncoded(calendar),
121
+ color: event.color?.toString(16)?.padStart(8, '0'),
122
+ });
109
123
  }
110
124
  catch (e) {
111
125
  await tx.rollback().execute();
@@ -128,7 +142,7 @@ addRoute({
128
142
  .catch(withError('Event does not exist', 404));
129
143
  if (event.calendar.userId != userId && event.attendees.every(a => a.userId != userId))
130
144
  error(403, 'You do not have access to this event');
131
- return event;
145
+ return withEncoded(event);
132
146
  },
133
147
  async PATCH(request, { id }) {
134
148
  const { attendees: attendeesInit = [], sendEmails, recurrenceUpdateAll, ...init } = await parseBody(request, EventInit);
@@ -145,7 +159,7 @@ addRoute({
145
159
  try {
146
160
  const event = await tx
147
161
  .updateTable('events')
148
- .set(init)
162
+ .set(withDecoded(init))
149
163
  .where('id', '=', id)
150
164
  .returningAll()
151
165
  .returning(withAttendees)
@@ -173,7 +187,7 @@ addRoute({
173
187
  */
174
188
  }
175
189
  await tx.commit().execute();
176
- return event;
190
+ return withEncoded(event);
177
191
  }
178
192
  catch (e) {
179
193
  await tx.rollback().execute();
@@ -198,7 +212,7 @@ addRoute({
198
212
  .where('id', '=', id)
199
213
  .executeTakeFirstOrThrow();
200
214
  await tx.commit().execute();
201
- return event;
215
+ return withEncoded(event);
202
216
  }
203
217
  catch (e) {
204
218
  await tx.rollback().execute();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/calendar",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "author": "James Prevett <axium@jamespre.dev>",
5
5
  "description": "Calendar for Axium",
6
6
  "funding": {
@@ -36,10 +36,10 @@
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@axium/client": ">=0.14.3",
39
- "@axium/core": ">=0.19.0",
39
+ "@axium/core": ">=0.20.0",
40
40
  "@axium/server": ">=0.36.0",
41
41
  "@sveltejs/kit": "^2.27.3",
42
- "utilium": "^2.6.3"
42
+ "utilium": "^2.7.0"
43
43
  },
44
44
  "dependencies": {
45
45
  "zod": "^4.0.5"
@@ -1,11 +1,12 @@
1
1
  <script lang="ts">
2
2
  import { getEvents, type EventInitFormData } from '@axium/calendar/client';
3
- import type { Event, EventData } from '@axium/calendar/common';
3
+ import type { Calendar, Event, EventData } from '@axium/calendar/common';
4
4
  import { AttendeeInit, dateToInputValue, formatEventTimes, getCalPermissionsInfo, weekDaysFor } from '@axium/calendar/common';
5
- import * as Calendar from '@axium/calendar/components';
5
+ import * as Cal from '@axium/calendar/components';
6
6
  import { contextMenu, dynamicRows } from '@axium/client/attachments';
7
- import { AccessControlDialog, FormDialog, Icon, Popover, UserDiscovery } from '@axium/client/components';
7
+ import { AccessControlDialog, ColorPicker, FormDialog, Icon, Popover, UserDiscovery } from '@axium/client/components';
8
8
  import { fetchAPI } from '@axium/client/requests';
9
+ import { colorHashHex, encodeColor, decodeColor } from '@axium/core/color';
9
10
  import { SvelteDate } from 'svelte/reactivity';
10
11
  import { _throw } from 'utilium';
11
12
  import z from 'zod';
@@ -23,6 +24,7 @@
23
24
  let events = $state<Event[]>([]);
24
25
 
25
26
  $effect(() => {
27
+ for (const cal of calendars) cal.color ||= encodeColor(colorHashHex(cal.name));
26
28
  getEvents(calendars, { start: new Date(start.getTime()), end: new Date(end.getTime()) }).then(e => (events = e));
27
29
  });
28
30
 
@@ -42,16 +44,20 @@
42
44
 
43
45
  let dialogs = $state<Record<string, HTMLDialogElement>>({});
44
46
 
45
- let eventInit = $state<EventData & { attendees: AttendeeInit[] }>({
46
- attendees: [],
47
- recurrenceExcludes: [],
48
- recurrenceId: null,
49
- calId: calendars[0]?.id,
50
- } as any),
47
+ const defaultEventInit = {
48
+ attendees: [],
49
+ recurrenceExcludes: [],
50
+ recurrenceId: null,
51
+ calId: calendars[0]?.id,
52
+ } as any;
53
+
54
+ let eventInit = $state<EventData & { attendees: AttendeeInit[]; calendar?: Calendar }>(defaultEventInit),
51
55
  eventInitStart = $derived(dateToInputValue(eventInit.start)),
52
56
  eventInitEnd = $derived(dateToInputValue(eventInit.end)),
53
57
  eventEditId = $state<string>(),
54
58
  eventEditCalId = $state<string>();
59
+
60
+ const defaultEventColor = $derived((eventInit.calendar || calendars[0])?.color || encodeColor(colorHashHex(user.name)));
55
61
  </script>
56
62
 
57
63
  <svelte:head>
@@ -79,7 +85,7 @@
79
85
  <span class="label">{weekDays[0].toLocaleString('default', { month: 'long', year: 'numeric' })}</span>
80
86
  </div>
81
87
  <div id="cal-list">
82
- <Calendar.Select bind:start bind:end />
88
+ <Cal.Select bind:start bind:end />
83
89
  <div class="cal-list-header">
84
90
  <h4>My Calendars</h4>
85
91
  <button style:display="contents" command="show-modal" commandfor="add-calendar">
@@ -170,7 +176,16 @@
170
176
  {#snippet toggle()}
171
177
  {@const start = event.start.getHours() * 60 + event.start.getMinutes()}
172
178
  {@const end = event.end.getHours() * 60 + event.end.getMinutes()}
173
- <div class="event" style:top="{start / 14.4}%" style:height="{(end - start) / 14.4}%">
179
+ <div
180
+ class="event"
181
+ style:top="{start / 14.4}%"
182
+ style:height="{(end - start) / 14.4}%"
183
+ style="--event-color:{decodeColor(
184
+ event.color ||
185
+ event.calendar!.color ||
186
+ encodeColor(colorHashHex(event.calendar!.name), true)
187
+ )}"
188
+ >
174
189
  <span>{event.summary}</span>
175
190
  <span class="subtle">{formatEventTimes(event)}</span>
176
191
  </div>
@@ -265,7 +280,7 @@
265
280
  <FormDialog
266
281
  id="event-init"
267
282
  clearOnCancel
268
- cancel={() => (eventInit = {} as any)}
283
+ cancel={() => (eventInit = defaultEventInit)}
269
284
  submitText={eventEditId ? 'Update' : 'Create'}
270
285
  submit={async (data: EventInitFormData) => {
271
286
  Object.assign(eventInit, data);
@@ -327,6 +342,7 @@
327
342
  <option value={cal.id}>{cal.name}</option>
328
343
  {/each}
329
344
  </select>
345
+ <ColorPicker bind:value={eventInit.color} defaultValue={defaultEventColor} />
330
346
  </div>
331
347
 
332
348
  <div class="attendees-container">
@@ -598,7 +614,7 @@
598
614
  position: absolute;
599
615
  border-radius: 0.5em;
600
616
  padding: 0.25em;
601
- background-color: var(--bg-alt);
617
+ background-color: var(--event-color, var(--bg-alt));
602
618
  display: flex;
603
619
  flex-direction: column;
604
620
  align-items: flex-start;