@axium/notes 0.3.14 → 0.4.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
@@ -59,6 +59,16 @@
59
59
  }
60
60
  }
61
61
  }
62
+ },
63
+ {
64
+ "delta": true,
65
+ "alter_tables": {
66
+ "notes": {
67
+ "add_columns": {
68
+ "pinned": { "type": "boolean", "required": true, "default": false }
69
+ }
70
+ }
71
+ }
62
72
  }
63
73
  ],
64
74
  "wipe": ["notes", "acl.notes"],
package/dist/common.d.ts CHANGED
@@ -3,6 +3,7 @@ export declare const NoteInit: z.ZodObject<{
3
3
  title: z.ZodString;
4
4
  content: z.ZodOptional<z.ZodNullable<z.ZodString>>;
5
5
  labels: z.ZodDefault<z.ZodArray<z.ZodString>>;
6
+ pinned: z.ZodDefault<z.ZodBoolean>;
6
7
  }, z.core.$strip>;
7
8
  export interface NoteInit extends z.infer<typeof NoteInit> {
8
9
  }
@@ -10,10 +11,32 @@ export declare const Note: z.ZodObject<{
10
11
  title: z.ZodString;
11
12
  content: z.ZodOptional<z.ZodNullable<z.ZodString>>;
12
13
  labels: z.ZodDefault<z.ZodArray<z.ZodString>>;
14
+ pinned: z.ZodDefault<z.ZodBoolean>;
13
15
  id: z.ZodUUID;
14
16
  userId: z.ZodUUID;
15
17
  created: z.ZodCoercedDate<unknown>;
16
18
  modified: z.ZodCoercedDate<unknown>;
19
+ acl: z.ZodArray<z.ZodObject<{
20
+ itemId: z.ZodUUID;
21
+ userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
22
+ role: z.ZodOptional<z.ZodNullable<z.ZodString>>;
23
+ tag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
24
+ user: z.ZodOptional<z.ZodNullable<z.ZodObject<{
25
+ id: z.ZodUUID;
26
+ name: z.ZodString;
27
+ email: z.ZodOptional<z.ZodEmail>;
28
+ emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
29
+ preferences: z.ZodOptional<z.ZodLazy<z.ZodObject<{
30
+ debug: z.ZodDefault<z.ZodBoolean>;
31
+ }, z.core.$strip>>>;
32
+ roles: z.ZodArray<z.ZodString>;
33
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
34
+ registeredAt: z.ZodCoercedDate<unknown>;
35
+ isAdmin: z.ZodOptional<z.ZodBoolean>;
36
+ isSuspended: z.ZodOptional<z.ZodBoolean>;
37
+ }, z.core.$strip>>>;
38
+ createdAt: z.ZodCoercedDate<unknown>;
39
+ }, z.core.$catchall<z.ZodBoolean>>>;
17
40
  }, z.core.$strip>;
18
41
  export interface Note extends z.infer<typeof Note> {
19
42
  }
@@ -23,23 +46,68 @@ declare const NotesAPI: {
23
46
  title: z.ZodString;
24
47
  content: z.ZodOptional<z.ZodNullable<z.ZodString>>;
25
48
  labels: z.ZodDefault<z.ZodArray<z.ZodString>>;
49
+ pinned: z.ZodDefault<z.ZodBoolean>;
26
50
  id: z.ZodUUID;
27
51
  userId: z.ZodUUID;
28
52
  created: z.ZodCoercedDate<unknown>;
29
53
  modified: z.ZodCoercedDate<unknown>;
54
+ acl: z.ZodArray<z.ZodObject<{
55
+ itemId: z.ZodUUID;
56
+ userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
57
+ role: z.ZodOptional<z.ZodNullable<z.ZodString>>;
58
+ tag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
59
+ user: z.ZodOptional<z.ZodNullable<z.ZodObject<{
60
+ id: z.ZodUUID;
61
+ name: z.ZodString;
62
+ email: z.ZodOptional<z.ZodEmail>;
63
+ emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
64
+ preferences: z.ZodOptional<z.ZodLazy<z.ZodObject<{
65
+ debug: z.ZodDefault<z.ZodBoolean>;
66
+ }, z.core.$strip>>>;
67
+ roles: z.ZodArray<z.ZodString>;
68
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
69
+ registeredAt: z.ZodCoercedDate<unknown>;
70
+ isAdmin: z.ZodOptional<z.ZodBoolean>;
71
+ isSuspended: z.ZodOptional<z.ZodBoolean>;
72
+ }, z.core.$strip>>>;
73
+ createdAt: z.ZodCoercedDate<unknown>;
74
+ }, z.core.$catchall<z.ZodBoolean>>>;
30
75
  }, z.core.$strip>>;
31
76
  readonly PUT: readonly [z.ZodObject<{
32
77
  title: z.ZodString;
33
78
  content: z.ZodOptional<z.ZodNullable<z.ZodString>>;
34
79
  labels: z.ZodDefault<z.ZodArray<z.ZodString>>;
80
+ pinned: z.ZodDefault<z.ZodBoolean>;
35
81
  }, z.core.$strip>, z.ZodObject<{
36
82
  title: z.ZodString;
37
83
  content: z.ZodOptional<z.ZodNullable<z.ZodString>>;
38
84
  labels: z.ZodDefault<z.ZodArray<z.ZodString>>;
85
+ pinned: z.ZodDefault<z.ZodBoolean>;
39
86
  id: z.ZodUUID;
40
87
  userId: z.ZodUUID;
41
88
  created: z.ZodCoercedDate<unknown>;
42
89
  modified: z.ZodCoercedDate<unknown>;
90
+ acl: z.ZodArray<z.ZodObject<{
91
+ itemId: z.ZodUUID;
92
+ userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
93
+ role: z.ZodOptional<z.ZodNullable<z.ZodString>>;
94
+ tag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
95
+ user: z.ZodOptional<z.ZodNullable<z.ZodObject<{
96
+ id: z.ZodUUID;
97
+ name: z.ZodString;
98
+ email: z.ZodOptional<z.ZodEmail>;
99
+ emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
100
+ preferences: z.ZodOptional<z.ZodLazy<z.ZodObject<{
101
+ debug: z.ZodDefault<z.ZodBoolean>;
102
+ }, z.core.$strip>>>;
103
+ roles: z.ZodArray<z.ZodString>;
104
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
105
+ registeredAt: z.ZodCoercedDate<unknown>;
106
+ isAdmin: z.ZodOptional<z.ZodBoolean>;
107
+ isSuspended: z.ZodOptional<z.ZodBoolean>;
108
+ }, z.core.$strip>>>;
109
+ createdAt: z.ZodCoercedDate<unknown>;
110
+ }, z.core.$catchall<z.ZodBoolean>>>;
43
111
  }, z.core.$strip>];
44
112
  };
45
113
  readonly 'notes/:id': {
@@ -47,32 +115,99 @@ declare const NotesAPI: {
47
115
  title: z.ZodString;
48
116
  content: z.ZodOptional<z.ZodNullable<z.ZodString>>;
49
117
  labels: z.ZodDefault<z.ZodArray<z.ZodString>>;
118
+ pinned: z.ZodDefault<z.ZodBoolean>;
50
119
  id: z.ZodUUID;
51
120
  userId: z.ZodUUID;
52
121
  created: z.ZodCoercedDate<unknown>;
53
122
  modified: z.ZodCoercedDate<unknown>;
123
+ acl: z.ZodArray<z.ZodObject<{
124
+ itemId: z.ZodUUID;
125
+ userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
126
+ role: z.ZodOptional<z.ZodNullable<z.ZodString>>;
127
+ tag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
128
+ user: z.ZodOptional<z.ZodNullable<z.ZodObject<{
129
+ id: z.ZodUUID;
130
+ name: z.ZodString;
131
+ email: z.ZodOptional<z.ZodEmail>;
132
+ emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
133
+ preferences: z.ZodOptional<z.ZodLazy<z.ZodObject<{
134
+ debug: z.ZodDefault<z.ZodBoolean>;
135
+ }, z.core.$strip>>>;
136
+ roles: z.ZodArray<z.ZodString>;
137
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
138
+ registeredAt: z.ZodCoercedDate<unknown>;
139
+ isAdmin: z.ZodOptional<z.ZodBoolean>;
140
+ isSuspended: z.ZodOptional<z.ZodBoolean>;
141
+ }, z.core.$strip>>>;
142
+ createdAt: z.ZodCoercedDate<unknown>;
143
+ }, z.core.$catchall<z.ZodBoolean>>>;
54
144
  }, z.core.$strip>;
55
145
  readonly PATCH: readonly [z.ZodObject<{
56
146
  title: z.ZodString;
57
147
  content: z.ZodOptional<z.ZodNullable<z.ZodString>>;
58
148
  labels: z.ZodDefault<z.ZodArray<z.ZodString>>;
149
+ pinned: z.ZodDefault<z.ZodBoolean>;
59
150
  }, z.core.$strip>, z.ZodObject<{
60
151
  title: z.ZodString;
61
152
  content: z.ZodOptional<z.ZodNullable<z.ZodString>>;
62
153
  labels: z.ZodDefault<z.ZodArray<z.ZodString>>;
154
+ pinned: z.ZodDefault<z.ZodBoolean>;
63
155
  id: z.ZodUUID;
64
156
  userId: z.ZodUUID;
65
157
  created: z.ZodCoercedDate<unknown>;
66
158
  modified: z.ZodCoercedDate<unknown>;
159
+ acl: z.ZodArray<z.ZodObject<{
160
+ itemId: z.ZodUUID;
161
+ userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
162
+ role: z.ZodOptional<z.ZodNullable<z.ZodString>>;
163
+ tag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
164
+ user: z.ZodOptional<z.ZodNullable<z.ZodObject<{
165
+ id: z.ZodUUID;
166
+ name: z.ZodString;
167
+ email: z.ZodOptional<z.ZodEmail>;
168
+ emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
169
+ preferences: z.ZodOptional<z.ZodLazy<z.ZodObject<{
170
+ debug: z.ZodDefault<z.ZodBoolean>;
171
+ }, z.core.$strip>>>;
172
+ roles: z.ZodArray<z.ZodString>;
173
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
174
+ registeredAt: z.ZodCoercedDate<unknown>;
175
+ isAdmin: z.ZodOptional<z.ZodBoolean>;
176
+ isSuspended: z.ZodOptional<z.ZodBoolean>;
177
+ }, z.core.$strip>>>;
178
+ createdAt: z.ZodCoercedDate<unknown>;
179
+ }, z.core.$catchall<z.ZodBoolean>>>;
67
180
  }, z.core.$strip>];
68
181
  readonly DELETE: z.ZodObject<{
69
182
  title: z.ZodString;
70
183
  content: z.ZodOptional<z.ZodNullable<z.ZodString>>;
71
184
  labels: z.ZodDefault<z.ZodArray<z.ZodString>>;
185
+ pinned: z.ZodDefault<z.ZodBoolean>;
72
186
  id: z.ZodUUID;
73
187
  userId: z.ZodUUID;
74
188
  created: z.ZodCoercedDate<unknown>;
75
189
  modified: z.ZodCoercedDate<unknown>;
190
+ acl: z.ZodArray<z.ZodObject<{
191
+ itemId: z.ZodUUID;
192
+ userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
193
+ role: z.ZodOptional<z.ZodNullable<z.ZodString>>;
194
+ tag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
195
+ user: z.ZodOptional<z.ZodNullable<z.ZodObject<{
196
+ id: z.ZodUUID;
197
+ name: z.ZodString;
198
+ email: z.ZodOptional<z.ZodEmail>;
199
+ emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
200
+ preferences: z.ZodOptional<z.ZodLazy<z.ZodObject<{
201
+ debug: z.ZodDefault<z.ZodBoolean>;
202
+ }, z.core.$strip>>>;
203
+ roles: z.ZodArray<z.ZodString>;
204
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
205
+ registeredAt: z.ZodCoercedDate<unknown>;
206
+ isAdmin: z.ZodOptional<z.ZodBoolean>;
207
+ isSuspended: z.ZodOptional<z.ZodBoolean>;
208
+ }, z.core.$strip>>>;
209
+ createdAt: z.ZodCoercedDate<unknown>;
210
+ }, z.core.$catchall<z.ZodBoolean>>>;
76
211
  }, z.core.$strip>;
77
212
  };
78
213
  };
package/dist/common.js CHANGED
@@ -1,15 +1,18 @@
1
+ import { AccessControl } from '@axium/core';
1
2
  import { $API } from '@axium/core/api';
2
3
  import * as z from 'zod';
3
4
  export const NoteInit = z.object({
4
5
  title: z.string().max(100),
5
6
  content: z.string().max(10_000).nullish(),
6
7
  labels: z.array(z.string().max(30)).default([]),
8
+ pinned: z.boolean().default(false),
7
9
  });
8
10
  export const Note = NoteInit.extend({
9
11
  id: z.uuid(),
10
12
  userId: z.uuid(),
11
13
  created: z.coerce.date(),
12
14
  modified: z.coerce.date(),
15
+ acl: AccessControl.array(),
13
16
  });
14
17
  const NotesAPI = {
15
18
  'users/:id/notes': {
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type schema from '../db.json';
2
1
  import type { FromFile as FromSchemaFile } from '@axium/server/db/schema';
2
+ import type schema from '../db.json';
3
3
  declare module '@axium/server/database' {
4
4
  interface Schema extends FromSchemaFile<typeof schema> {
5
5
  }
package/dist/server.js CHANGED
@@ -1,3 +1,4 @@
1
+ import * as acl from '@axium/server/acl';
1
2
  import { authRequestForItem, checkAuthForUser } from '@axium/server/auth';
2
3
  import { database } from '@axium/server/database';
3
4
  import { parseBody, withError } from '@axium/server/requests';
@@ -8,23 +9,26 @@ addRoute({
8
9
  path: '/api/users/:id/notes',
9
10
  params: { id: z.uuid() },
10
11
  async GET(request, { id: userId }) {
11
- await checkAuthForUser(request, userId);
12
+ const { user } = await checkAuthForUser(request, userId);
12
13
  return await database
13
14
  .selectFrom('notes')
14
15
  .selectAll()
15
- .where('userId', '=', userId)
16
+ .select(acl.from('notes'))
17
+ .where(eb => eb.or([eb('userId', '=', userId), acl.existsIn('notes', user)(eb)]))
18
+ .orderBy('pinned', 'desc')
19
+ .orderBy('modified', 'desc')
16
20
  .execute()
17
21
  .catch(withError('Could not get notes'));
18
22
  },
19
23
  async PUT(request, { id: userId }) {
20
24
  const init = await parseBody(request, NoteInit);
21
25
  await checkAuthForUser(request, userId);
22
- return await database
26
+ return Object.assign(await database
23
27
  .insertInto('notes')
24
28
  .values({ ...init, userId })
25
29
  .returningAll()
26
30
  .executeTakeFirstOrThrow()
27
- .catch(withError('Could not create note'));
31
+ .catch(withError('Could not create note')), { acl: [] });
28
32
  },
29
33
  });
30
34
  addRoute({
@@ -43,6 +47,7 @@ addRoute({
43
47
  .set('modified', new Date())
44
48
  .where('id', '=', id)
45
49
  .returningAll()
50
+ .returning(acl.from('notes'))
46
51
  .executeTakeFirstOrThrow()
47
52
  .catch(withError('Could not update note'));
48
53
  },
@@ -52,6 +57,7 @@ addRoute({
52
57
  .deleteFrom('notes')
53
58
  .where('id', '=', id)
54
59
  .returningAll()
60
+ .returning(acl.from('notes'))
55
61
  .executeTakeFirstOrThrow()
56
62
  .catch(withError('Could not delete note'));
57
63
  },
package/lib/Note.svelte CHANGED
@@ -6,6 +6,7 @@
6
6
  import { AccessControlDialog, Icon, Popover } from '@axium/client/components';
7
7
  import { copy } from '@axium/client/gui';
8
8
  import { toastStatus } from '@axium/client/toast';
9
+ import { checkAndMatchACL, type UserPublic } from '@axium/core';
9
10
  import type { Note } from '@axium/notes/common';
10
11
  import { download } from 'utilium/dom';
11
12
 
@@ -16,50 +17,66 @@
16
17
  user,
17
18
  }: { note: Note; notes?: Note[]; pageMode?: boolean; user?: UserPublic } = $props();
18
19
 
19
- let acl = $state<HTMLDialogElement>();
20
+ const canEdit = user && (user.id === note.userId || !checkAndMatchACL(note.acl, user, { edit: true }).size);
21
+ const canManage = user && (user.id === note.userId || !checkAndMatchACL(note.acl, user, { manage: true }).size);
20
22
  </script>
21
23
 
22
24
  <div class={['note', pageMode && 'full-page']}>
23
25
  <div class="note-header">
24
- <input
25
- type="text"
26
- bind:value={note.title}
27
- placeholder="Unnamed Note"
28
- class="editable-text"
29
- oninput={e => {
30
- note.title = e.currentTarget.value;
31
- fetchAPI('PATCH', 'notes/:id', note, note.id);
32
- }}
33
- />
26
+ {#if note.pinned}
27
+ <div class="pin"><Icon i="thumbtack" /></div>
28
+ {/if}
29
+ {#if canEdit}
30
+ <input
31
+ type="text"
32
+ bind:value={note.title}
33
+ placeholder="Unnamed Note"
34
+ class="note-title"
35
+ oninput={e => {
36
+ note.title = e.currentTarget.value;
37
+ fetchAPI('PATCH', 'notes/:id', note, note.id);
38
+ }}
39
+ />
40
+ {:else}
41
+ <span class="note-title">{note.title}</span>
42
+ {/if}
34
43
  <Popover showToggle="hover">
35
- <div
36
- class="menu-item"
37
- onclick={() =>
38
- toastStatus(
39
- fetchAPI('DELETE', 'notes/:id', {}, note.id).then(() => {
40
- if (!notes) goto('/notes');
41
- else notes.splice(notes.indexOf(note), 1);
42
- }),
43
- text('notes.toast_deleted')
44
- )}
45
- >
46
- <Icon i="trash" />
47
- <span>{text('generic.delete')}</span>
48
- </div>
44
+ {#if canEdit}
45
+ <div
46
+ class="menu-item"
47
+ onclick={() => {
48
+ note.pinned = !note.pinned;
49
+ fetchAPI('PATCH', 'notes/:id', note, note.id);
50
+ }}
51
+ >
52
+ <Icon i="thumbtack{note.pinned ? '-slash' : ''}" />
53
+ <span>{note.pinned ? text('notes.unpin') : text('notes.pin')}</span>
54
+ </div>
55
+ {/if}
56
+ {#if canManage}
57
+ <div
58
+ class="menu-item"
59
+ onclick={() =>
60
+ toastStatus(
61
+ fetchAPI('DELETE', 'notes/:id', {}, note.id).then(() => {
62
+ if (!notes) goto('/notes');
63
+ else notes.splice(notes.indexOf(note), 1);
64
+ }),
65
+ text('notes.toast_deleted')
66
+ )}
67
+ >
68
+ <Icon i="trash" />
69
+ <span>{text('generic.delete')}</span>
70
+ </div>
71
+ {/if}
49
72
  <div class="menu-item" onclick={() => download(note.title + '.txt', note.content ?? '')}>
50
73
  <Icon i="download" />
51
74
  <span>{text('notes.download')}</span>
52
75
  </div>
53
- <div
54
- class="menu-item"
55
- onclick={() => {
56
- acl!.showModal();
57
- acl!.click();
58
- }}
59
- >
76
+ <button class="reset menu-item" command="show-modal" commandfor="acl#{note.id}">
60
77
  <Icon i="user-group" />
61
78
  <span>{text('notes.share')}</span>
62
- </div>
79
+ </button>
63
80
  <div class="menu-item" onclick={() => copy('text/plain', `${location.origin}/notes/${note.id}`)}>
64
81
  <Icon i="link-horizontal" />
65
82
  <span>{text('notes.copy_link')}</span>
@@ -77,24 +94,39 @@
77
94
  </div>
78
95
  {/if}
79
96
  </Popover>
80
- <AccessControlDialog bind:dialog={acl} item={note} itemType="notes" {user} />
97
+ <AccessControlDialog item={note} itemType="notes" {user} id="acl#{note.id}" />
81
98
  </div>
82
- <textarea
83
- bind:value={note.content}
84
- name="content"
85
- class="editable-text"
86
- placeholder={text('notes.content_placeholder')}
87
- oninput={() => fetchAPI('PATCH', 'notes/:id', note, note.id)}
88
- {@attach dynamicRows()}>{note.content}</textarea
89
- >
99
+ {#if canEdit}
100
+ <textarea
101
+ bind:value={note.content}
102
+ name="content"
103
+ class="note-content"
104
+ placeholder={text('notes.content_placeholder')}
105
+ oninput={() => fetchAPI('PATCH', 'notes/:id', note, note.id)}
106
+ {@attach dynamicRows()}>{note.content}</textarea
107
+ >
108
+ {:else}
109
+ <div class="note-content">
110
+ {#if note.content}
111
+ <span>{note.content}</span>
112
+ {:else}
113
+ <i class="subtle">{text('notes.content_missing')}</i>
114
+ {/if}
115
+ </div>
116
+ {/if}
90
117
  </div>
91
118
 
92
119
  <style>
93
- .editable-text {
120
+ .note-title,
121
+ .note-content {
94
122
  background: none;
95
123
  border: none;
96
124
  }
97
125
 
126
+ div.note-content i.subtle {
127
+ font-size: inherit;
128
+ }
129
+
98
130
  .note {
99
131
  display: flex;
100
132
  flex-direction: column;
@@ -106,13 +138,6 @@
106
138
  height: fit-content;
107
139
  max-height: 40em;
108
140
  anchor-name: --note;
109
-
110
- textarea {
111
- resize: none;
112
- field-sizing: content;
113
- height: max-content;
114
- overflow-y: scroll;
115
- }
116
141
  }
117
142
 
118
143
  .note.full-page {
@@ -125,7 +150,11 @@
125
150
  justify-content: space-between;
126
151
  align-items: center;
127
152
 
128
- input {
153
+ .pin {
154
+ flex: 0 0 auto;
155
+ }
156
+
157
+ .note-title {
129
158
  font-size: 1.5em;
130
159
  font-weight: bold;
131
160
  padding: 0;
package/locales/en.json CHANGED
@@ -5,13 +5,16 @@
5
5
  "notes": {
6
6
  "back_to_main": "Back to Notes",
7
7
  "content_placeholder": "It's a beautiful day outside...",
8
+ "content_missing": "This note is shared with you but has no content.",
8
9
  "copy_id": "Copy ID",
9
- "copy_link": "Copy Link",
10
+ "copy_link": "Copy link",
10
11
  "download": "Download",
11
- "new": "New Note",
12
+ "new": "New note",
12
13
  "note_title": "Notes — {title}",
13
- "open_new_tab": "Open in New Tab",
14
+ "open_new_tab": "Open in new tab",
14
15
  "share": "Share",
15
- "toast_deleted": "Note deleted"
16
+ "toast_deleted": "Note deleted",
17
+ "pin": "Pin note",
18
+ "unpin": "Unpin note"
16
19
  }
17
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/notes",
3
- "version": "0.3.14",
3
+ "version": "0.4.0",
4
4
  "author": "James Prevett <axium@jamespre.dev>",
5
5
  "description": "Notes for Axium",
6
6
  "funding": {
@@ -7,6 +7,9 @@
7
7
  const { data } = $props();
8
8
 
9
9
  let notes = $state(data.notes);
10
+ $effect(() => {
11
+ notes.sort((a, b) => +!!b.pinned - +!!a.pinned);
12
+ });
10
13
  </script>
11
14
 
12
15
  <svelte:head>
@@ -48,6 +51,7 @@
48
51
 
49
52
  .lists-container {
50
53
  display: grid;
54
+ display: grid-lanes;
51
55
  grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
52
56
  gap: 1em;
53
57
  }