@glw907/cairn-cms 0.58.0 → 0.59.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/CHANGELOG.md +37 -0
- package/dist/components/CairnMediaLibrary.svelte +1101 -27
- package/dist/components/CairnMediaLibrary.svelte.d.ts +10 -2
- package/dist/components/admin-icons.d.ts +1 -0
- package/dist/components/admin-icons.js +1 -0
- package/dist/components/cairn-admin.css +147 -0
- package/dist/log/events.d.ts +1 -1
- package/dist/media/bulk-delete-plan.d.ts +24 -0
- package/dist/media/bulk-delete-plan.js +25 -0
- package/dist/media/orphan-scan.d.ts +37 -0
- package/dist/media/orphan-scan.js +42 -0
- package/dist/media/reconcile.d.ts +3 -0
- package/dist/media/reconcile.js +3 -2
- package/dist/sveltekit/cairn-admin.d.ts +3 -0
- package/dist/sveltekit/cairn-admin.js +6 -0
- package/dist/sveltekit/content-routes.d.ts +37 -4
- package/dist/sveltekit/content-routes.js +247 -1
- package/dist/sveltekit/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/lib/components/CairnMediaLibrary.svelte +1101 -27
- package/src/lib/components/admin-icons.ts +1 -0
- package/src/lib/log/events.ts +2 -0
- package/src/lib/media/bulk-delete-plan.ts +54 -0
- package/src/lib/media/orphan-scan.ts +74 -0
- package/src/lib/media/reconcile.ts +3 -2
- package/src/lib/sveltekit/cairn-admin.ts +6 -0
- package/src/lib/sveltekit/content-routes.ts +293 -5
- package/src/lib/sveltekit/index.ts +1 -0
|
@@ -13,8 +13,16 @@ interface Props {
|
|
|
13
13
|
* shows where each one is used, edits its name and default alt, and deletes it safely. The resting
|
|
14
14
|
* surface is a visual contact-sheet grid (a roving-tabindex listbox of tiles), with a list-density
|
|
15
15
|
* toggle that flips to an enriched sortable table. One toolbar row carries search, a pick-one triage
|
|
16
|
-
* radiogroup (All, Needs alt,
|
|
17
|
-
* client window all run over the full loaded set in component state.
|
|
16
|
+
* radiogroup (All, Needs alt, No references found), and the density toggle. Filtering, sorting, and a
|
|
17
|
+
* growing client window all run over the full loaded set in component state.
|
|
18
|
+
*
|
|
19
|
+
* Multi-select rides a Set of selected hashes, decoupled from the slide-over's single asset and from
|
|
20
|
+
* roving focus. The grid is an APG multiselectable listbox (aria-multiselectable, real cell focus):
|
|
21
|
+
* Space toggles the focused tile, Shift+Arrow extends a range, Ctrl/Cmd+A selects every visible asset,
|
|
22
|
+
* and Escape clears. The list density is a plain selectable table whose leading native-checkbox column
|
|
23
|
+
* is the selection signal (no grid role, since it has no grid keyboard model). A sticky action bar
|
|
24
|
+
* appears on the first selection with a live count, the scope, Select all in view, Clear, and the
|
|
25
|
+
* reversible bulk Delete.
|
|
18
26
|
*
|
|
19
27
|
* Activating a tile or row opens a NON-MODAL detail slide-over from the right (the established
|
|
20
28
|
* details-slide-over recipe): no scrim, the library stays live and in the a11y tree behind it, Escape
|
|
@@ -27,3 +27,4 @@ export { default as RefreshCwIcon } from '@lucide/svelte/icons/refresh-cw';
|
|
|
27
27
|
export { default as GitBranchIcon } from '@lucide/svelte/icons/git-branch';
|
|
28
28
|
export { default as ArrowRightIcon } from '@lucide/svelte/icons/arrow-right';
|
|
29
29
|
export { default as MegaphoneIcon } from '@lucide/svelte/icons/megaphone';
|
|
30
|
+
export { default as DatabaseIcon } from '@lucide/svelte/icons/database';
|
|
@@ -29,3 +29,4 @@ export { default as RefreshCwIcon } from '@lucide/svelte/icons/refresh-cw';
|
|
|
29
29
|
export { default as GitBranchIcon } from '@lucide/svelte/icons/git-branch';
|
|
30
30
|
export { default as ArrowRightIcon } from '@lucide/svelte/icons/arrow-right';
|
|
31
31
|
export { default as MegaphoneIcon } from '@lucide/svelte/icons/megaphone';
|
|
32
|
+
export { default as DatabaseIcon } from '@lucide/svelte/icons/database';
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
--container-md: 28rem;
|
|
81
81
|
--container-lg: 32rem;
|
|
82
82
|
--container-xl: 36rem;
|
|
83
|
+
--container-2xl: 42rem;
|
|
83
84
|
--container-3xl: 48rem;
|
|
84
85
|
--container-5xl: 64rem;
|
|
85
86
|
--text-xs: .75rem;
|
|
@@ -3046,6 +3047,14 @@
|
|
|
3046
3047
|
bottom: calc(var(--spacing) * 0);
|
|
3047
3048
|
}
|
|
3048
3049
|
|
|
3050
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .bottom-3\.5 {
|
|
3051
|
+
bottom: calc(var(--spacing) * 3.5);
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .left-2 {
|
|
3055
|
+
left: calc(var(--spacing) * 2);
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3049
3058
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .join {
|
|
3050
3059
|
--join-ss: 0;
|
|
3051
3060
|
--join-se: 0;
|
|
@@ -3348,6 +3357,10 @@
|
|
|
3348
3357
|
z-index: 10;
|
|
3349
3358
|
}
|
|
3350
3359
|
|
|
3360
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .z-20 {
|
|
3361
|
+
z-index: 20;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3351
3364
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .z-30 {
|
|
3352
3365
|
z-index: 30;
|
|
3353
3366
|
}
|
|
@@ -4309,10 +4322,22 @@
|
|
|
4309
4322
|
height: 1px;
|
|
4310
4323
|
}
|
|
4311
4324
|
|
|
4325
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-h-36 {
|
|
4326
|
+
max-height: calc(var(--spacing) * 36);
|
|
4327
|
+
}
|
|
4328
|
+
|
|
4329
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-h-40 {
|
|
4330
|
+
max-height: calc(var(--spacing) * 40);
|
|
4331
|
+
}
|
|
4332
|
+
|
|
4312
4333
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-h-44 {
|
|
4313
4334
|
max-height: calc(var(--spacing) * 44);
|
|
4314
4335
|
}
|
|
4315
4336
|
|
|
4337
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-h-52 {
|
|
4338
|
+
max-height: calc(var(--spacing) * 52);
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4316
4341
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-h-56 {
|
|
4317
4342
|
max-height: calc(var(--spacing) * 56);
|
|
4318
4343
|
}
|
|
@@ -4513,6 +4538,10 @@
|
|
|
4513
4538
|
max-width: calc(var(--spacing) * 0);
|
|
4514
4539
|
}
|
|
4515
4540
|
|
|
4541
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-2xl {
|
|
4542
|
+
max-width: var(--container-2xl);
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4516
4545
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-3xl {
|
|
4517
4546
|
max-width: var(--container-3xl);
|
|
4518
4547
|
}
|
|
@@ -4541,6 +4570,10 @@
|
|
|
4541
4570
|
max-width: 72ch;
|
|
4542
4571
|
}
|
|
4543
4572
|
|
|
4573
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-\[640px\] {
|
|
4574
|
+
max-width: 640px;
|
|
4575
|
+
}
|
|
4576
|
+
|
|
4544
4577
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-full {
|
|
4545
4578
|
max-width: 100%;
|
|
4546
4579
|
}
|
|
@@ -4695,6 +4728,10 @@
|
|
|
4695
4728
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
4696
4729
|
}
|
|
4697
4730
|
|
|
4731
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .grid-cols-3 {
|
|
4732
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
4733
|
+
}
|
|
4734
|
+
|
|
4698
4735
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .grid-cols-\[1fr_auto_1fr\] {
|
|
4699
4736
|
grid-template-columns: 1fr auto 1fr;
|
|
4700
4737
|
}
|
|
@@ -4871,6 +4908,10 @@
|
|
|
4871
4908
|
border-radius: .25rem;
|
|
4872
4909
|
}
|
|
4873
4910
|
|
|
4911
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .rounded-2xl {
|
|
4912
|
+
border-radius: var(--radius-2xl);
|
|
4913
|
+
}
|
|
4914
|
+
|
|
4874
4915
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .rounded-\[0\.55rem\] {
|
|
4875
4916
|
border-radius: .55rem;
|
|
4876
4917
|
}
|
|
@@ -5001,6 +5042,26 @@
|
|
|
5001
5042
|
}
|
|
5002
5043
|
}
|
|
5003
5044
|
|
|
5045
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .border-\[color-mix\(in_oklab\,var\(--cairn-card-border\)_70\%\,transparent\)\] {
|
|
5046
|
+
border-color: var(--cairn-card-border);
|
|
5047
|
+
}
|
|
5048
|
+
|
|
5049
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
5050
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .border-\[color-mix\(in_oklab\,var\(--cairn-card-border\)_70\%\,transparent\)\] {
|
|
5051
|
+
border-color: color-mix(in oklab,var(--cairn-card-border) 70%,transparent);
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
|
|
5055
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .border-\[color-mix\(in_oklab\,var\(--cairn-error-border\)_70\%\,transparent\)\] {
|
|
5056
|
+
border-color: var(--cairn-error-border);
|
|
5057
|
+
}
|
|
5058
|
+
|
|
5059
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
5060
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .border-\[color-mix\(in_oklab\,var\(--cairn-error-border\)_70\%\,transparent\)\] {
|
|
5061
|
+
border-color: color-mix(in oklab,var(--cairn-error-border) 70%,transparent);
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
|
|
5004
5065
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .border-\[color-mix\(in_oklab\,var\(--color-error\)_35\%\,var\(--cairn-card-border\)\)\] {
|
|
5005
5066
|
border-color: var(--color-error);
|
|
5006
5067
|
}
|
|
@@ -5105,6 +5166,16 @@
|
|
|
5105
5166
|
border: none;
|
|
5106
5167
|
}
|
|
5107
5168
|
|
|
5169
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .bg-\[color-mix\(in_oklab\,var\(--cairn-warning-ink\)_8\%\,var\(--color-base-100\)\)\] {
|
|
5170
|
+
background-color: var(--cairn-warning-ink);
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
5174
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .bg-\[color-mix\(in_oklab\,var\(--cairn-warning-ink\)_8\%\,var\(--color-base-100\)\)\] {
|
|
5175
|
+
background-color: color-mix(in oklab,var(--cairn-warning-ink) 8%,var(--color-base-100));
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
|
|
5108
5179
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .bg-\[color-mix\(in_oklab\,var\(--color-error\)_5\%\,transparent\)\] {
|
|
5109
5180
|
background-color: var(--color-error);
|
|
5110
5181
|
}
|
|
@@ -5157,6 +5228,14 @@
|
|
|
5157
5228
|
}
|
|
5158
5229
|
}
|
|
5159
5230
|
|
|
5231
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .bg-\[var\(--color-error\)\] {
|
|
5232
|
+
background-color: var(--color-error);
|
|
5233
|
+
}
|
|
5234
|
+
|
|
5235
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .bg-\[var\(--color-positive-tint\,var\(--cairn-card-border\)\)\] {
|
|
5236
|
+
background-color: var(--color-positive-tint, var(--cairn-card-border));
|
|
5237
|
+
}
|
|
5238
|
+
|
|
5160
5239
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .bg-base-100 {
|
|
5161
5240
|
background-color: var(--color-base-100);
|
|
5162
5241
|
}
|
|
@@ -5273,6 +5352,16 @@
|
|
|
5273
5352
|
}
|
|
5274
5353
|
}
|
|
5275
5354
|
|
|
5355
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .bg-primary\/\[0\.03\] {
|
|
5356
|
+
background-color: var(--color-primary);
|
|
5357
|
+
}
|
|
5358
|
+
|
|
5359
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
5360
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .bg-primary\/\[0\.03\] {
|
|
5361
|
+
background-color: color-mix(in oklab, var(--color-primary) 3%, transparent);
|
|
5362
|
+
}
|
|
5363
|
+
}
|
|
5364
|
+
|
|
5276
5365
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .bg-primary\/\[0\.05\] {
|
|
5277
5366
|
background-color: var(--color-primary);
|
|
5278
5367
|
}
|
|
@@ -5305,12 +5394,30 @@
|
|
|
5305
5394
|
background-color: var(--color-warning);
|
|
5306
5395
|
}
|
|
5307
5396
|
|
|
5397
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .\[background-image\:linear-gradient\(45deg\,color-mix\(in_oklab\,var\(--color-base-content\)_7\%\,transparent\)_25\%\,transparent_25\%\,transparent_75\%\,color-mix\(in_oklab\,var\(--color-base-content\)_7\%\,transparent\)_75\%\)\,linear-gradient\(45deg\,color-mix\(in_oklab\,var\(--color-base-content\)_7\%\,transparent\)_25\%\,transparent_25\%\,transparent_75\%\,color-mix\(in_oklab\,var\(--color-base-content\)_7\%\,transparent\)_75\%\)\] {
|
|
5398
|
+
background-image: linear-gradient(45deg,var(--color-base-content) 25%,transparent 25%,transparent 75%,var(--color-base-content) 75%),linear-gradient(45deg,var(--color-base-content) 25%,transparent 25%,transparent 75%,var(--color-base-content) 75%);
|
|
5399
|
+
}
|
|
5400
|
+
|
|
5401
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
5402
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .\[background-image\:linear-gradient\(45deg\,color-mix\(in_oklab\,var\(--color-base-content\)_7\%\,transparent\)_25\%\,transparent_25\%\,transparent_75\%\,color-mix\(in_oklab\,var\(--color-base-content\)_7\%\,transparent\)_75\%\)\,linear-gradient\(45deg\,color-mix\(in_oklab\,var\(--color-base-content\)_7\%\,transparent\)_25\%\,transparent_25\%\,transparent_75\%\,color-mix\(in_oklab\,var\(--color-base-content\)_7\%\,transparent\)_75\%\)\] {
|
|
5403
|
+
background-image: linear-gradient(45deg,color-mix(in oklab,var(--color-base-content) 7%,transparent) 25%,transparent 25%,transparent 75%,color-mix(in oklab,var(--color-base-content) 7%,transparent) 75%),linear-gradient(45deg,color-mix(in oklab,var(--color-base-content) 7%,transparent) 25%,transparent 25%,transparent 75%,color-mix(in oklab,var(--color-base-content) 7%,transparent) 75%);
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
5406
|
+
|
|
5308
5407
|
@layer daisyui.l1.l2 {
|
|
5309
5408
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .loading-spinner {
|
|
5310
5409
|
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
|
|
5311
5410
|
}
|
|
5312
5411
|
}
|
|
5313
5412
|
|
|
5413
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .\[background-size\:8px_8px\] {
|
|
5414
|
+
background-size: 8px 8px;
|
|
5415
|
+
}
|
|
5416
|
+
|
|
5417
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .\[background-position\:0_0\,4px_4px\] {
|
|
5418
|
+
background-position: 0 0, 4px 4px;
|
|
5419
|
+
}
|
|
5420
|
+
|
|
5314
5421
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .object-contain {
|
|
5315
5422
|
object-fit: contain;
|
|
5316
5423
|
}
|
|
@@ -5464,6 +5571,10 @@
|
|
|
5464
5571
|
padding-inline: calc(var(--spacing) * 3);
|
|
5465
5572
|
}
|
|
5466
5573
|
|
|
5574
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .px-3\.5 {
|
|
5575
|
+
padding-inline: calc(var(--spacing) * 3.5);
|
|
5576
|
+
}
|
|
5577
|
+
|
|
5467
5578
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .px-4 {
|
|
5468
5579
|
padding-inline: calc(var(--spacing) * 4);
|
|
5469
5580
|
}
|
|
@@ -5786,6 +5897,10 @@
|
|
|
5786
5897
|
word-break: break-all;
|
|
5787
5898
|
}
|
|
5788
5899
|
|
|
5900
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .whitespace-nowrap {
|
|
5901
|
+
white-space: nowrap;
|
|
5902
|
+
}
|
|
5903
|
+
|
|
5789
5904
|
@layer daisyui.l1.l2 {
|
|
5790
5905
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .alert-error {
|
|
5791
5906
|
color: var(--color-error-content);
|
|
@@ -5822,6 +5937,10 @@
|
|
|
5822
5937
|
color: var(--color-accent);
|
|
5823
5938
|
}
|
|
5824
5939
|
|
|
5940
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .text-\[var\(--color-error-content\)\] {
|
|
5941
|
+
color: var(--color-error-content);
|
|
5942
|
+
}
|
|
5943
|
+
|
|
5825
5944
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .text-\[var\(--color-muted\)\] {
|
|
5826
5945
|
color: var(--color-muted);
|
|
5827
5946
|
}
|
|
@@ -6005,6 +6124,16 @@
|
|
|
6005
6124
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
6006
6125
|
}
|
|
6007
6126
|
|
|
6127
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .ring-primary\/40 {
|
|
6128
|
+
--tw-ring-color: var(--color-primary);
|
|
6129
|
+
}
|
|
6130
|
+
|
|
6131
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
6132
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .ring-primary\/40 {
|
|
6133
|
+
--tw-ring-color: color-mix(in oklab, var(--color-primary) 40%, transparent);
|
|
6134
|
+
}
|
|
6135
|
+
}
|
|
6136
|
+
|
|
6008
6137
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .ring-primary\/70 {
|
|
6009
6138
|
--tw-ring-color: var(--color-primary);
|
|
6010
6139
|
}
|
|
@@ -6343,6 +6472,24 @@
|
|
|
6343
6472
|
}
|
|
6344
6473
|
}
|
|
6345
6474
|
|
|
6475
|
+
@media (hover: hover) {
|
|
6476
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .hover\:bg-\[var\(--cairn-error-tint\)\]:hover {
|
|
6477
|
+
background-color: var(--cairn-error-tint);
|
|
6478
|
+
}
|
|
6479
|
+
}
|
|
6480
|
+
|
|
6481
|
+
@media (hover: hover) {
|
|
6482
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .hover\:bg-\[var\(--color-error\)\]\/90:hover {
|
|
6483
|
+
background-color: var(--color-error);
|
|
6484
|
+
}
|
|
6485
|
+
|
|
6486
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
6487
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .hover\:bg-\[var\(--color-error\)\]\/90:hover {
|
|
6488
|
+
background-color: color-mix(in oklab, var(--color-error) 90%, transparent);
|
|
6489
|
+
}
|
|
6490
|
+
}
|
|
6491
|
+
}
|
|
6492
|
+
|
|
6346
6493
|
@media (hover: hover) {
|
|
6347
6494
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .hover\:bg-base-200:hover {
|
|
6348
6495
|
background-color: var(--color-base-200);
|
package/dist/log/events.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export type CairnLogEvent = 'auth.link.requested' | 'auth.link.send_failed' | 'auth.token.minted' | 'auth.token.confirmed' | 'auth.session.created' | 'auth.session.destroyed' | 'commit.succeeded' | 'commit.failed' | 'config.invalid' | 'entry.published' | 'entry.discarded' | 'publish.failed' | 'github.unreachable' | 'guard.rejected' | 'media.uploaded' | 'media.upload_failed' | 'media.delivery_failed' | 'media.orphan_reconcile' | 'media.resolve_missing' | 'media.deleted' | 'media.delete_blocked' | 'media.replaced' | 'media.replace_blocked' | 'media.alt_propagated';
|
|
1
|
+
export type CairnLogEvent = 'auth.link.requested' | 'auth.link.send_failed' | 'auth.token.minted' | 'auth.token.confirmed' | 'auth.session.created' | 'auth.session.destroyed' | 'commit.succeeded' | 'commit.failed' | 'config.invalid' | 'entry.published' | 'entry.discarded' | 'publish.failed' | 'github.unreachable' | 'guard.rejected' | 'media.uploaded' | 'media.upload_failed' | 'media.delivery_failed' | 'media.orphan_reconcile' | 'media.resolve_missing' | 'media.deleted' | 'media.delete_blocked' | 'media.bulk_deleted' | 'media.orphans_purged' | 'media.replaced' | 'media.replace_blocked' | 'media.alt_propagated';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { UsageEntry, UsageIndex } from './usage.js';
|
|
2
|
+
import type { MediaManifest } from './manifest.js';
|
|
3
|
+
/** One selected hash that is not deleted, with why and (for the where-used) its usage rows. The rows
|
|
4
|
+
* are present only for 'still-referenced'; an 'uncommitted' skip carries an empty list. */
|
|
5
|
+
export interface BulkDeleteSkip {
|
|
6
|
+
hash: string;
|
|
7
|
+
reason: 'still-referenced' | 'uncommitted';
|
|
8
|
+
usage: UsageEntry[];
|
|
9
|
+
}
|
|
10
|
+
/** The partitioned selection: the hashes safe to purge and the hashes held back. Both arrays keep the
|
|
11
|
+
* input order of `selected` so the screen reports them in the order the user picked. */
|
|
12
|
+
export interface BulkDeletePlan {
|
|
13
|
+
deletable: string[];
|
|
14
|
+
skipped: BulkDeleteSkip[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Partition `selected` against a strict usage index and the media manifest.
|
|
18
|
+
*
|
|
19
|
+
* A hash with one or more usage rows is skipped 'still-referenced', carrying those rows for the
|
|
20
|
+
* where-used. A hash with no usage row and no committed manifest row is skipped 'uncommitted', since
|
|
21
|
+
* there is nothing committed to delete. A hash with no usage row and a committed manifest row is
|
|
22
|
+
* deletable. The input order of `selected` is preserved in both output arrays.
|
|
23
|
+
*/
|
|
24
|
+
export declare function planBulkDelete(selected: string[], index: UsageIndex, manifest: MediaManifest): BulkDeletePlan;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partition `selected` against a strict usage index and the media manifest.
|
|
3
|
+
*
|
|
4
|
+
* A hash with one or more usage rows is skipped 'still-referenced', carrying those rows for the
|
|
5
|
+
* where-used. A hash with no usage row and no committed manifest row is skipped 'uncommitted', since
|
|
6
|
+
* there is nothing committed to delete. A hash with no usage row and a committed manifest row is
|
|
7
|
+
* deletable. The input order of `selected` is preserved in both output arrays.
|
|
8
|
+
*/
|
|
9
|
+
export function planBulkDelete(selected, index, manifest) {
|
|
10
|
+
const deletable = [];
|
|
11
|
+
const skipped = [];
|
|
12
|
+
for (const hash of selected) {
|
|
13
|
+
const usage = index.get(hash);
|
|
14
|
+
if (usage && usage.length > 0) {
|
|
15
|
+
skipped.push({ hash, reason: 'still-referenced', usage });
|
|
16
|
+
}
|
|
17
|
+
else if (manifest[hash]) {
|
|
18
|
+
deletable.push(hash);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
skipped.push({ hash, reason: 'uncommitted', usage: [] });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { deletable, skipped };
|
|
25
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ReconcileResult } from './reconcile.js';
|
|
2
|
+
import type { MediaManifest } from './manifest.js';
|
|
3
|
+
import type { UsageEntry, UsageIndex } from './usage.js';
|
|
4
|
+
/** A purgeable orphan: a stored R2 key with no manifest row, plus the 16-hex hash parsed from it. */
|
|
5
|
+
export interface OrphanByteRow {
|
|
6
|
+
/** The full R2 object key, e.g. "media/ff/ffffffffffffffff.webp". */
|
|
7
|
+
key: string;
|
|
8
|
+
/** The 16-hex content hash parsed from the key. */
|
|
9
|
+
hash: string;
|
|
10
|
+
}
|
|
11
|
+
/** A broken reference: a manifest row whose bytes are gone. Read-only, since purging it would drop a
|
|
12
|
+
* still-referenced asset's record; the screen shows where it is used so an operator can re-ingest. */
|
|
13
|
+
export interface BrokenRefRow {
|
|
14
|
+
/** The 16-hex content hash of the manifest row whose bytes are missing. */
|
|
15
|
+
hash: string;
|
|
16
|
+
/** The manifest row's display slug, or '' when the row is somehow absent. */
|
|
17
|
+
slug: string;
|
|
18
|
+
/** Where the asset is referenced, from the usage index. Empty when no reference was found. */
|
|
19
|
+
usage: UsageEntry[];
|
|
20
|
+
}
|
|
21
|
+
/** The scan surface model: the two row sets the Library renders. */
|
|
22
|
+
export interface OrphanScan {
|
|
23
|
+
orphanedBytes: OrphanByteRow[];
|
|
24
|
+
brokenRefs: BrokenRefRow[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Project a reconcile read plus the usage index into the scan surface model.
|
|
28
|
+
*
|
|
29
|
+
* `orphanedBytes` come from `reconcile.orphanedObjects`: each key is parsed to its hash via the
|
|
30
|
+
* shared media-key grammar, and a key that does not match (so it is not a content-addressed media
|
|
31
|
+
* object) is skipped. A key whose hash the usage index references is also skipped: it is referenced
|
|
32
|
+
* on main or some open branch, so its bytes are in use, not orphaned. `brokenRefs` come from
|
|
33
|
+
* `reconcile.missingObjects`: each hash carries its
|
|
34
|
+
* manifest slug (falling back to '' when the row is absent) and its where-used rows from the index
|
|
35
|
+
* (an empty list when no reference was found). Both directions keep their input order.
|
|
36
|
+
*/
|
|
37
|
+
export declare function buildOrphanScan(reconcile: ReconcileResult, manifest: MediaManifest, index: UsageIndex): OrphanScan;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// cairn-cms: the orphan-scan projection, the pure model behind the admin Media Library's scan
|
|
2
|
+
// surface. It folds reconcileMedia's two directions together with the usage index into the two rows
|
|
3
|
+
// the screen renders: the purgeable byte-rows and the read-only broken-reference rows (manifest rows
|
|
4
|
+
// whose bytes are gone). It only projects; no path here reads R2, the manifest, or git. The module
|
|
5
|
+
// is engine-internal and on no public subpath.
|
|
6
|
+
//
|
|
7
|
+
// An orphaned byte is a stored R2 object whose hash has NO manifest row AND appears in NO usage row,
|
|
8
|
+
// so it is referenced nowhere across main and every open branch. Reconcile only checks main's
|
|
9
|
+
// manifest, so a branch-only upload (bytes in R2, manifest row only on the open cairn/* branch) gets
|
|
10
|
+
// flagged as an orphaned object even though a colleague's in-progress draft references it. The byte
|
|
11
|
+
// purge is irreversible, so we intersect reconcile's verdict with the strict cross-branch usage
|
|
12
|
+
// index here: any hash the index references is in use and is dropped from orphanedBytes, which keeps
|
|
13
|
+
// a live draft's bytes from ever reaching the purge surface.
|
|
14
|
+
import { MEDIA_KEY_RE } from './reconcile.js';
|
|
15
|
+
/**
|
|
16
|
+
* Project a reconcile read plus the usage index into the scan surface model.
|
|
17
|
+
*
|
|
18
|
+
* `orphanedBytes` come from `reconcile.orphanedObjects`: each key is parsed to its hash via the
|
|
19
|
+
* shared media-key grammar, and a key that does not match (so it is not a content-addressed media
|
|
20
|
+
* object) is skipped. A key whose hash the usage index references is also skipped: it is referenced
|
|
21
|
+
* on main or some open branch, so its bytes are in use, not orphaned. `brokenRefs` come from
|
|
22
|
+
* `reconcile.missingObjects`: each hash carries its
|
|
23
|
+
* manifest slug (falling back to '' when the row is absent) and its where-used rows from the index
|
|
24
|
+
* (an empty list when no reference was found). Both directions keep their input order.
|
|
25
|
+
*/
|
|
26
|
+
export function buildOrphanScan(reconcile, manifest, index) {
|
|
27
|
+
const orphanedBytes = [];
|
|
28
|
+
for (const key of reconcile.orphanedObjects) {
|
|
29
|
+
const hash = MEDIA_KEY_RE.exec(key)?.[1];
|
|
30
|
+
if (hash === undefined)
|
|
31
|
+
continue;
|
|
32
|
+
if (index.has(hash))
|
|
33
|
+
continue;
|
|
34
|
+
orphanedBytes.push({ key, hash });
|
|
35
|
+
}
|
|
36
|
+
const brokenRefs = reconcile.missingObjects.map((hash) => ({
|
|
37
|
+
hash,
|
|
38
|
+
slug: manifest[hash]?.slug ?? '',
|
|
39
|
+
usage: index.get(hash) ?? [],
|
|
40
|
+
}));
|
|
41
|
+
return { orphanedBytes, brokenRefs };
|
|
42
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { MediaManifest } from './manifest.js';
|
|
2
|
+
/** A stored media object key parses to its short hash via `media/<aa>/<shortHash>.<ext>`. Exported so
|
|
3
|
+
* the orphan-scan projection derives the same hash from an orphaned key without a second grammar. */
|
|
4
|
+
export declare const MEDIA_KEY_RE: RegExp;
|
|
2
5
|
/** What a reconcile read found in either direction. `orphanedObjects` are stored R2 keys whose hash
|
|
3
6
|
* has no manifest row; `missingObjects` are manifest hashes with no stored object. */
|
|
4
7
|
export interface ReconcileResult {
|
package/dist/media/reconcile.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { log } from '../log/index.js';
|
|
2
|
-
/** A stored media object key parses to its short hash via `media/<aa>/<shortHash>.<ext>`.
|
|
3
|
-
|
|
2
|
+
/** A stored media object key parses to its short hash via `media/<aa>/<shortHash>.<ext>`. Exported so
|
|
3
|
+
* the orphan-scan projection derives the same hash from an orphaned key without a second grammar. */
|
|
4
|
+
export const MEDIA_KEY_RE = /^media\/[0-9a-f]{2}\/([0-9a-f]{16})\.[a-z0-9]{1,5}$/;
|
|
4
5
|
/** The pure core: compare the stored R2 keys against the manifest's content-hash keys and report
|
|
5
6
|
* both orphan directions. A stored key that does not match the media-key grammar is ignored, since
|
|
6
7
|
* it is not a content-addressed media object this reconcile owns. */
|
|
@@ -86,6 +86,9 @@ export declare function createCairnAdmin(runtime: CairnRuntime, deps?: CairnAdmi
|
|
|
86
86
|
mediaReplace: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
87
87
|
mediaAltPreview: (event: AdminEvent) => Promise<import("./content-routes.js").MediaAltPreviewPlan | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
88
88
|
mediaAltPropagate: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
89
|
+
mediaBulkDelete: (event: AdminEvent) => Promise<import("./content-routes.js").MediaBulkDeleteResult | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
90
|
+
mediaOrphanScan: (event: AdminEvent) => Promise<import("../media/orphan-scan.js").OrphanScan | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
91
|
+
mediaPurge: (event: AdminEvent) => Promise<import("./content-routes.js").MediaOrphanPurgeResult | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
89
92
|
publishAll: (event: AdminEvent) => Promise<never>;
|
|
90
93
|
addEditor: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<{
|
|
91
94
|
error: string;
|
|
@@ -134,6 +134,12 @@ export function createCairnAdmin(runtime, deps = {}) {
|
|
|
134
134
|
mediaReplace: viewAction(['media'], (event) => content.mediaReplaceApply(contentEvent(event, {}))),
|
|
135
135
|
mediaAltPreview: viewAction(['media'], (event) => content.mediaAltPreview(contentEvent(event, {}))),
|
|
136
136
|
mediaAltPropagate: viewAction(['media'], (event) => content.mediaAltApply(contentEvent(event, {}))),
|
|
137
|
+
// Pass C library actions: a multi-select bulk delete, the on-demand orphan scan, and the
|
|
138
|
+
// irreversible byte purge. The component posts to `?/mediaBulkDelete`, `?/mediaOrphanScan`, and
|
|
139
|
+
// `?/mediaPurge` (the purge key is short of its content method name). All gate on the media view.
|
|
140
|
+
mediaBulkDelete: viewAction(['media'], (event) => content.mediaBulkDelete(contentEvent(event, {}))),
|
|
141
|
+
mediaOrphanScan: viewAction(['media'], (event) => content.mediaOrphanScan(contentEvent(event, {}))),
|
|
142
|
+
mediaPurge: viewAction(['media'], (event) => content.mediaPurgeOrphans(contentEvent(event, {}))),
|
|
137
143
|
publishAll: viewAction(authedViews, (event) => content.publishAllAction(contentEvent(event, {}))),
|
|
138
144
|
addEditor: viewAction(['editors'], (event) => editors.addEditorAction(event)),
|
|
139
145
|
removeEditor: viewAction(['editors'], (event) => editors.removeEditorAction(event)),
|
|
@@ -4,8 +4,10 @@ import { type LinkTarget, type InboundLink } from '../content/manifest.js';
|
|
|
4
4
|
import type { MediaEntry } from '../media/manifest.js';
|
|
5
5
|
import type { MediaLibrary, MediaLibraryEntry } from '../media/library-entry.js';
|
|
6
6
|
import type { UsageEntry } from '../media/usage.js';
|
|
7
|
+
import { type OrphanScan } from '../media/orphan-scan.js';
|
|
7
8
|
import type { RepointPlacement, AltPlacement } from '../content/media-rewrite.js';
|
|
8
9
|
import type { BranchRef } from '../media/rewrite-plan.js';
|
|
10
|
+
import type { BulkDeleteSkip } from '../media/bulk-delete-plan.js';
|
|
9
11
|
import type { CookieJar, EventBase } from './types.js';
|
|
10
12
|
import type { CairnRuntime, FrontmatterField, ResolvedPreview } from '../content/types.js';
|
|
11
13
|
import type { Role } from '../auth/types.js';
|
|
@@ -135,9 +137,10 @@ export interface MediaLibraryData {
|
|
|
135
137
|
* redirected commit conflict never overwrite each other. */
|
|
136
138
|
error: string | null;
|
|
137
139
|
/** The success flash a redirected action carries: `deleted` from `?deleted=1`, `updated` from
|
|
138
|
-
* `?updated=1`, `replaced` from `?replaced=1`, `altPropagated` from `?altPropagated=1`,
|
|
139
|
-
*
|
|
140
|
-
|
|
140
|
+
* `?updated=1`, `replaced` from `?replaced=1`, `altPropagated` from `?altPropagated=1`,
|
|
141
|
+
* `bulkDeleted` from `?bulkDeleted=1`, `orphansPurged` from `?orphansPurged=1`, null otherwise.
|
|
142
|
+
* The component renders a polite success strip for each. */
|
|
143
|
+
flash: 'deleted' | 'updated' | 'replaced' | 'altPropagated' | 'bulkDeleted' | 'orphansPurged' | null;
|
|
141
144
|
/** A redirected action's conflict error read from `?error=` (a commit-conflict bounce). Kept in
|
|
142
145
|
* its own slot rather than the degraded-load `error` above, so the two never collide. */
|
|
143
146
|
flashError: string | null;
|
|
@@ -212,6 +215,33 @@ export interface MediaReplaceFailure {
|
|
|
212
215
|
export interface MediaAltPropagateFailure {
|
|
213
216
|
error: string;
|
|
214
217
|
}
|
|
218
|
+
/** A refused media bulk delete or orphan purge: `fail(503)` for the fail-closed strict-usage refusal
|
|
219
|
+
* (the whole batch refuses) or media-off / a missing bucket binding. The per-item outcomes ride the
|
|
220
|
+
* returned summary, not a fail. */
|
|
221
|
+
export interface MediaBulkFailure {
|
|
222
|
+
error: string;
|
|
223
|
+
}
|
|
224
|
+
/** The bulk-delete outcome the component renders: the deleted hashes, the skipped rows from the
|
|
225
|
+
* partition (with their reason and where-used), and any per-object R2 delete failure. Admin-internal,
|
|
226
|
+
* not on the package subpath, so no reference page. */
|
|
227
|
+
export interface MediaBulkDeleteResult {
|
|
228
|
+
deleted: string[];
|
|
229
|
+
skipped: BulkDeleteSkip[];
|
|
230
|
+
failed: {
|
|
231
|
+
hash: string;
|
|
232
|
+
error: string;
|
|
233
|
+
}[];
|
|
234
|
+
}
|
|
235
|
+
/** The orphan-purge outcome: the purged R2 keys, the keys skipped because their hash was claimed by a
|
|
236
|
+
* manifest row since the scan, and any per-object delete failure. Admin-internal, no reference page. */
|
|
237
|
+
export interface MediaOrphanPurgeResult {
|
|
238
|
+
purged: string[];
|
|
239
|
+
skippedClaimed: string[];
|
|
240
|
+
failed: {
|
|
241
|
+
key: string;
|
|
242
|
+
error: string;
|
|
243
|
+
}[];
|
|
244
|
+
}
|
|
215
245
|
/** One entry the replace preview will rewrite, enriched with its display title and permalink from the
|
|
216
246
|
* content manifest (the planner's PlannedEntry carries neither). The screen lists these as the
|
|
217
247
|
* confirm dialog's where-touched preview, and the apply re-derives its own plan rather than trusting
|
|
@@ -275,7 +305,7 @@ export interface MediaAltPreviewPlan {
|
|
|
275
305
|
* keys identify which guard refused. The media refusals ride here too, so the Media Library's one
|
|
276
306
|
* `form` prop carries a `?/mediaDelete`, `?/mediaUpdate`, `?/mediaReplace`, or `?/mediaAltPropagate`
|
|
277
307
|
* refusal without a second type. */
|
|
278
|
-
export type ContentFormFailure = Partial<SaveFailure & DeleteRefusal & RenameFailure & MediaDeleteRefusal & MediaUpdateFailure & MediaReplaceFailure & MediaAltPropagateFailure>;
|
|
308
|
+
export type ContentFormFailure = Partial<SaveFailure & DeleteRefusal & RenameFailure & MediaDeleteRefusal & MediaUpdateFailure & MediaReplaceFailure & MediaAltPropagateFailure & MediaBulkFailure>;
|
|
279
309
|
/** The successful upload's response (`uploadAction`). The server-owned `record` rides the editor's
|
|
280
310
|
* optimistic client state and commits with the entry at Save (the upload itself commits nothing).
|
|
281
311
|
* `reused` is true when identical bytes were already stored, so the second upload did no second put;
|
|
@@ -302,6 +332,9 @@ export declare function createContentRoutes(runtime: CairnRuntime, deps?: Conten
|
|
|
302
332
|
renameAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
|
|
303
333
|
uploadAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | UploadResult>;
|
|
304
334
|
mediaDeleteAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
|
|
335
|
+
mediaBulkDelete: (event: ContentEvent) => Promise<ReturnType<typeof fail> | MediaBulkDeleteResult>;
|
|
336
|
+
mediaOrphanScan: (event: ContentEvent) => Promise<ReturnType<typeof fail> | OrphanScan>;
|
|
337
|
+
mediaPurgeOrphans: (event: ContentEvent) => Promise<ReturnType<typeof fail> | MediaOrphanPurgeResult>;
|
|
305
338
|
mediaUpdateAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
|
|
306
339
|
mediaReplacePreview: (event: ContentEvent) => Promise<ReturnType<typeof fail> | MediaReplacePreviewPlan>;
|
|
307
340
|
mediaReplaceApply: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
|