@conduction/nextcloud-vue 2.64.0 → 2.65.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.
Files changed (51) hide show
  1. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +99 -3
  2. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +23 -7
  4. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  5. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js +13 -0
  6. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
  7. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +2 -1
  8. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
  9. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +26 -2
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +6 -3
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  13. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +64 -5
  14. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  15. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +66 -28
  16. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  17. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +22 -5
  18. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  19. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +1 -1
  20. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  21. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  22. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  23. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +22 -3
  24. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  25. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +10 -3
  26. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  27. package/dist/esm/nextcloud-vue.css +164 -0
  28. package/dist/esm/store/useObjectStore.js +118 -33
  29. package/dist/esm/store/useObjectStore.js.map +1 -1
  30. package/dist/esm/utils/actionsDispatcher.js +14 -0
  31. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  32. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  33. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  34. package/dist/nextcloud-vue.cjs.js +488 -96
  35. package/dist/nextcloud-vue.cjs.js.map +2 -2
  36. package/dist/nextcloud-vue.css +164 -0
  37. package/package.json +1 -1
  38. package/src/components/CnAppNav/CnAppNav.vue +148 -7
  39. package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +14 -0
  40. package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -2
  41. package/src/components/CnDetailPage/CnDetailPage.md +5 -1
  42. package/src/components/CnDetailPage/CnDetailPage.vue +114 -6
  43. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +34 -6
  44. package/src/components/CnStagesWidget/CnStagesWidget.vue +9 -0
  45. package/src/components/CnStatWidget/CnStatWidget.vue +33 -5
  46. package/src/css/detail-page.css +22 -0
  47. package/src/css/kpi-card.css +47 -0
  48. package/src/schemas/app-manifest-v2.schema.json +23 -2
  49. package/src/store/useObjectStore.js +118 -33
  50. package/src/utils/actionsDispatcher.js +14 -0
  51. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -1283,6 +1283,53 @@
1283
1283
  font-size: var(--cn-kpi-value-size-stacked, 2rem);
1284
1284
  }
1285
1285
 
1286
+ /* Stacked: the board's tile (`content.layout: 'stacked'`). A plain muted
1287
+ label, the number at 34px/700 in the text colour, and the caption on a line
1288
+ of its own; CnStatWidget draws no icon circle in this layout. The padding
1289
+ adds up with a dashboard card-fit cell's 8px 14px to the board's 18px 20px.
1290
+ Every value is a token so a theme can move it. */
1291
+ .cn-kpi-card--stacked {
1292
+ flex-direction: column;
1293
+ align-items: stretch;
1294
+ justify-content: center;
1295
+ gap: 0;
1296
+ padding: var(--cn-kpi-stacked-padding, 10px 6px);
1297
+ }
1298
+
1299
+ .cn-kpi-card--stacked .cn-kpi-card__body {
1300
+ gap: var(--cn-kpi-stacked-gap, 6px);
1301
+ }
1302
+
1303
+ .cn-kpi-card--stacked .cn-kpi-card__title {
1304
+ font-size: var(--cn-kpi-stacked-title-size, 14px);
1305
+ font-weight: var(--cn-kpi-stacked-title-weight, 400);
1306
+ color: var(--color-text-maxcontrast);
1307
+ }
1308
+
1309
+ .cn-kpi-card--stacked .cn-kpi-card__value {
1310
+ font-size: var(--cn-kpi-stacked-value-size, 34px);
1311
+ font-weight: var(--cn-kpi-value-weight, 700);
1312
+ line-height: 1;
1313
+ letter-spacing: -0.01em;
1314
+ color: var(--cn-kpi-stacked-value-color, var(--color-main-text));
1315
+ }
1316
+
1317
+ .cn-kpi-card--stacked .cn-kpi-card__caption-line {
1318
+ flex: 0 0 auto;
1319
+ max-width: 100%;
1320
+ font-size: var(--cn-kpi-label-size, 13px);
1321
+ font-weight: 500;
1322
+ }
1323
+
1324
+ /* A linked stacked tile reads as a tile, not as underlined link text. The
1325
+ NL Design theme underlines every `a` with `!important`, so the opt-out
1326
+ needs it too; it only applies to this opt-in layout. */
1327
+ a.cn-kpi-card--stacked,
1328
+ a.cn-kpi-card--stacked:hover,
1329
+ a.cn-kpi-card--stacked:focus-visible {
1330
+ text-decoration: none !important;
1331
+ }
1332
+
1286
1333
  /* Filled: the card draws its own grey box, for a KPI that is NOT already
1287
1334
  inside a wrapper that draws one. Opt-in via `content.flat: false`. */
1288
1335
  .cn-kpi-card--filled {
@@ -2790,6 +2837,28 @@
2790
2837
  min-width: 0;
2791
2838
  }
2792
2839
 
2840
+ /* A widget in the header (`config.headerWidget`) takes a row of its own under
2841
+ the title and the actions. */
2842
+ .cn-detail-page__header--with-widget {
2843
+ flex-wrap: wrap;
2844
+ row-gap: var(--cn-detail-header-row-gap, 22px);
2845
+ }
2846
+
2847
+ .cn-detail-page__header-widget {
2848
+ flex: 1 0 100%;
2849
+ min-width: 0;
2850
+ }
2851
+
2852
+ /* The header as a card (`config.headerCard`): the board's case header, a
2853
+ bordered box on the surface colour. The 56px inset that clears the
2854
+ navigation toggle is not needed inside a card that starts below it. */
2855
+ .cn-detail-page__header--card {
2856
+ padding: var(--cn-detail-header-card-padding, 22px 24px);
2857
+ border: 1px solid var(--color-border);
2858
+ border-radius: var(--cn-detail-header-card-radius, 12px);
2859
+ background: var(--color-main-background);
2860
+ }
2861
+
2793
2862
  .cn-detail-page__header-left {
2794
2863
  display: flex;
2795
2864
  align-items: center;
@@ -16875,7 +16944,17 @@ a.cn-xwiki-card__title[data-v-54e85fe3]:hover {
16875
16944
  text-overflow: ellipsis;
16876
16945
  white-space: nowrap;
16877
16946
  }
16947
+
16948
+ /* A clickable label is a <button>, and Nextcloud's core stylesheet gives
16949
+ every `button:not(.button-vue)` a min-height of the clickable area (34 to
16950
+ 44px) with the text centred in it. That pushed each reachable stage's label
16951
+ about 12px below the current one, which is a <span>. Zeroed here so all
16952
+ labels sit 8px under their bar, as the board draws them. The visually
16953
+ hidden "(current step)" is absolutely positioned and takes no space. */
16878
16954
  button.cn-stages-widget__bar-label[data-v-358fde0f] {
16955
+ min-height: 0;
16956
+ height: auto;
16957
+ line-height: inherit;
16879
16958
  cursor: pointer;
16880
16959
  }
16881
16960
  button.cn-stages-widget__bar-label[aria-disabled="true"][data-v-358fde0f] {
@@ -17310,6 +17389,53 @@ button.cn-stages-widget__bar-label[data-v-358fde0f]:focus-visible {
17310
17389
  font-size: var(--cn-kpi-value-size-stacked, 2rem);
17311
17390
  }
17312
17391
 
17392
+ /* Stacked: the board's tile (`content.layout: 'stacked'`). A plain muted
17393
+ label, the number at 34px/700 in the text colour, and the caption on a line
17394
+ of its own; CnStatWidget draws no icon circle in this layout. The padding
17395
+ adds up with a dashboard card-fit cell's 8px 14px to the board's 18px 20px.
17396
+ Every value is a token so a theme can move it. */
17397
+ .cn-kpi-card--stacked {
17398
+ flex-direction: column;
17399
+ align-items: stretch;
17400
+ justify-content: center;
17401
+ gap: 0;
17402
+ padding: var(--cn-kpi-stacked-padding, 10px 6px);
17403
+ }
17404
+
17405
+ .cn-kpi-card--stacked .cn-kpi-card__body {
17406
+ gap: var(--cn-kpi-stacked-gap, 6px);
17407
+ }
17408
+
17409
+ .cn-kpi-card--stacked .cn-kpi-card__title {
17410
+ font-size: var(--cn-kpi-stacked-title-size, 14px);
17411
+ font-weight: var(--cn-kpi-stacked-title-weight, 400);
17412
+ color: var(--color-text-maxcontrast);
17413
+ }
17414
+
17415
+ .cn-kpi-card--stacked .cn-kpi-card__value {
17416
+ font-size: var(--cn-kpi-stacked-value-size, 34px);
17417
+ font-weight: var(--cn-kpi-value-weight, 700);
17418
+ line-height: 1;
17419
+ letter-spacing: -0.01em;
17420
+ color: var(--cn-kpi-stacked-value-color, var(--color-main-text));
17421
+ }
17422
+
17423
+ .cn-kpi-card--stacked .cn-kpi-card__caption-line {
17424
+ flex: 0 0 auto;
17425
+ max-width: 100%;
17426
+ font-size: var(--cn-kpi-label-size, 13px);
17427
+ font-weight: 500;
17428
+ }
17429
+
17430
+ /* A linked stacked tile reads as a tile, not as underlined link text. The
17431
+ NL Design theme underlines every `a` with `!important`, so the opt-out
17432
+ needs it too; it only applies to this opt-in layout. */
17433
+ a.cn-kpi-card--stacked,
17434
+ a.cn-kpi-card--stacked:hover,
17435
+ a.cn-kpi-card--stacked:focus-visible {
17436
+ text-decoration: none !important;
17437
+ }
17438
+
17313
17439
  /* Filled: the card draws its own grey box, for a KPI that is NOT already
17314
17440
  inside a wrapper that draws one. Opt-in via `content.flat: false`. */
17315
17441
  .cn-kpi-card--filled {
@@ -18262,6 +18388,16 @@ to { background-position: -200% 0;
18262
18388
  flex: 0 0 auto;
18263
18389
  padding: 16px;
18264
18390
  }
18391
+
18392
+ /* On the page ground (`content.ground`): no card, so no inset around the
18393
+ view switch, and the heading at the board's 32px. */
18394
+ .cn-header-widget--ground .cn-header-widget__views[data-v-1b97430f] {
18395
+ padding: 0;
18396
+ }
18397
+ .cn-header-widget--ground .cn-header-widget__title[data-v-1b97430f] {
18398
+ font-size: var(--cn-header-ground-title-size, 32px);
18399
+ letter-spacing: -0.01em;
18400
+ }
18265
18401
  .cn-header-widget__date[data-v-1b97430f] {
18266
18402
  font-size: 14px;
18267
18403
  line-height: 1.4;
@@ -33247,6 +33383,21 @@ a.cn-integration-card__title[data-v-f7e9bbfa]:hover {
33247
33383
  padding: calc(var(--default-grid-baseline, 4px) * 2);
33248
33384
  }
33249
33385
 
33386
+ /*
33387
+ * The primary action never shrinks out of sight. NcAppNavigation renders it in
33388
+ * `.app-navigation__body`, a flex child with `overflow-y: scroll` (so its
33389
+ * minimum height is 0) beside a list of `height: 100%` and the footer. With a
33390
+ * card and footer entries below the list, the column overflows and the body
33391
+ * gives up its share of the shrink: "New case" showed as a 24px sliver of a
33392
+ * 42px button. All three forms carry `.app-navigation-new` (the solid and
33393
+ * link wrappers mirror NcAppNavigationNew's class), and the rule is scoped to
33394
+ * this component's navigation, so a navigation without a primary action lays
33395
+ * out exactly as before.
33396
+ */
33397
+ .app-navigation[data-testid="cn-nav"] .app-navigation__body:has(> .app-navigation-new) {
33398
+ flex-shrink: 0;
33399
+ }
33400
+
33250
33401
  /*
33251
33402
  * Footer-section items (section: "footer") render in NcAppNavigation's
33252
33403
  * #footer slot, outside the scroll container, so they stay visible above
@@ -33270,6 +33421,19 @@ a.cn-integration-card__title[data-v-f7e9bbfa]:hover {
33270
33421
  overflow: visible !important;
33271
33422
  }
33272
33423
 
33424
+ /*
33425
+ * A declared footer that names `settings` first (`nav.footer`) puts the
33426
+ * foldout above the footer list. The two are siblings in NcAppNavigation's
33427
+ * flex column, so `order` moves them without changing either's markup; the
33428
+ * card above keeps order 0 and stays first.
33429
+ */
33430
+ .cn-app-nav__settings--first {
33431
+ order: 1;
33432
+ }
33433
+ .cn-app-nav__footer-list--after-settings {
33434
+ order: 2;
33435
+ }
33436
+
33273
33437
  /*
33274
33438
  * Align the settings foldout's items with the main/footer list items,
33275
33439
  * whose icons sit at a 16px inset. The foldout's items live in a bare
@@ -264,6 +264,27 @@ const baseActions = {
264
264
  this.facets = { ...this.facets, [slug]: {} };
265
265
  },
266
266
 
267
+ /**
268
+ * Give a registered type a second schema path segment to try once when
269
+ * its own answers 404 (see `_fetchWithSchemaFallback`). Set by
270
+ * `resolveObjectOpType` for a schema title it kebab-cased, with the
271
+ * title as written. A type that is not registered is left alone.
272
+ *
273
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-schema-title-falls-back-to-its-written-form
274
+ * @param {string} slug The type slug
275
+ * @param {string} schema The fallback schema path segment
276
+ */
277
+ setSchemaFallback(slug, schema) {
278
+ const config = this.objectTypeRegistry[slug];
279
+ if (!config || typeof schema !== 'string' || schema === '') {
280
+ return
281
+ }
282
+ this.objectTypeRegistry = {
283
+ ...this.objectTypeRegistry,
284
+ [slug]: { ...config, schemaFallback: schema },
285
+ };
286
+ },
287
+
267
288
  /**
268
289
  * Unregister an object type and clean up all its state.
269
290
  *
@@ -433,11 +454,12 @@ const baseActions = {
433
454
  *
434
455
  * @param {string} type The type slug
435
456
  * @param {string|null} [id] Optional object ID
457
+ * @param {string|null} [schema] Schema path segment to use instead of the type's own
436
458
  * @return {string} Full API URL path
437
459
  */
438
- _buildUrl(type, id = null) {
460
+ _buildUrl(type, id = null, schema = null) {
439
461
  const config = this._getTypeConfig(type);
440
- let url = `${this._options.baseUrl}/${config.register}/${config.schema}`;
462
+ let url = `${this._options.baseUrl}/${config.register}/${schema ?? config.schema}`;
441
463
  if (id) {
442
464
  url += `/${id}`;
443
465
  }
@@ -458,11 +480,12 @@ const baseActions = {
458
480
  * @param {string} type The type slug
459
481
  * @param {object} [params] Extra query parameters
460
482
  * @param {string|null} [id] Optional object ID
483
+ * @param {string|null} [schema] Schema path segment to use instead of the type's own
461
484
  * @return {string} Full API URL path including query string
462
485
  */
463
- _buildUrlWithParams(type, params = {}, id = null) {
486
+ _buildUrlWithParams(type, params = {}, id = null, schema = null) {
464
487
  const config = this._getTypeConfig(type);
465
- let url = `${this._options.baseUrl}/${config.register}/${config.schema}`;
488
+ let url = `${this._options.baseUrl}/${config.register}/${schema ?? config.schema}`;
466
489
  if (id) {
467
490
  url += `/${id}`;
468
491
  }
@@ -472,6 +495,54 @@ const baseActions = {
472
495
  return url
473
496
  },
474
497
 
498
+ /**
499
+ * Fetch for a type, retrying ONCE with its `schemaFallback` when the
500
+ * schema path segment answers 404. A type without a fallback (every type
501
+ * not registered by `resolveObjectOpType` for a kebab-cased title) makes
502
+ * exactly the one request it always made. When the retry succeeds the
503
+ * fallback becomes the type's schema, so later calls go straight to it;
504
+ * when it does not, the first answer is returned and nothing changes.
505
+ *
506
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-schema-title-falls-back-to-its-written-form
507
+ * @param {string} type The type slug
508
+ * @param {function((string|null)): string} makeUrl Builds the URL; called with
509
+ * null for the type's own schema, or with the fallback segment.
510
+ * @param {object} init The fetch options.
511
+ * @return {Promise<Response>} The response to use.
512
+ */
513
+ async _fetchWithSchemaFallback(type, makeUrl, init) {
514
+ // `makeUrl` wraps `_buildUrl` / `_buildUrlWithParams`, so `url` is built
515
+ // from the prefixed `_options.baseUrl` like every other request here.
516
+ let url = makeUrl(null);
517
+ const response = await fetch(url, init);
518
+ if (!response || response.status !== 404) {
519
+ return response
520
+ }
521
+ const config = this.objectTypeRegistry[type];
522
+ const fallback = config && config.schemaFallback;
523
+ if (!fallback || String(fallback) === String(config.schema)) {
524
+ return response
525
+ }
526
+ let retry;
527
+ try {
528
+ url = makeUrl(fallback);
529
+ retry = await fetch(url, init);
530
+ } catch {
531
+ return response
532
+ }
533
+ if (!retry || !retry.ok) {
534
+ discardResponseBody(retry);
535
+ return response
536
+ }
537
+ discardResponseBody(response);
538
+ const { schemaFallback: _, ...rest } = config;
539
+ this.objectTypeRegistry = {
540
+ ...this.objectTypeRegistry,
541
+ [type]: { ...rest, schema: fallback },
542
+ };
543
+ return retry
544
+ },
545
+
475
546
  /**
476
547
  * Clear the error state for a type.
477
548
  *
@@ -604,12 +675,14 @@ const baseActions = {
604
675
  }
605
676
  }
606
677
 
607
- const url = this._buildUrlWithParams(type, fetchParams);
608
-
609
- const response = await fetch(url, {
610
- method: 'GET',
611
- headers: this._buildHeaders(),
612
- });
678
+ const response = await this._fetchWithSchemaFallback(
679
+ type,
680
+ (schema) => this._buildUrlWithParams(type, fetchParams, null, schema),
681
+ {
682
+ method: 'GET',
683
+ headers: this._buildHeaders(),
684
+ },
685
+ );
613
686
 
614
687
  if (!response.ok) {
615
688
  this.errors = { ...this.errors, [type]: await parseResponseError(response, type) };
@@ -675,11 +748,14 @@ const baseActions = {
675
748
  */
676
749
  async fetchCollectionForOptions(type, params = {}) {
677
750
  try {
678
- const url = this._buildUrlWithParams(type, params);
679
- const response = await fetch(url, {
680
- method: 'GET',
681
- headers: this._buildHeaders(),
682
- });
751
+ const response = await this._fetchWithSchemaFallback(
752
+ type,
753
+ (schema) => this._buildUrlWithParams(type, params, null, schema),
754
+ {
755
+ method: 'GET',
756
+ headers: this._buildHeaders(),
757
+ },
758
+ );
683
759
 
684
760
  if (!response.ok) {
685
761
  discardResponseBody(response);
@@ -734,10 +810,14 @@ const baseActions = {
734
810
  this.errors = { ...this.errors, [type]: null };
735
811
 
736
812
  try {
737
- const response = await fetch(url, {
738
- method: 'GET',
739
- headers: this._buildHeaders(),
740
- });
813
+ const response = await this._fetchWithSchemaFallback(
814
+ type,
815
+ (schema) => (schema === null ? url : this._buildUrl(type, id, schema)),
816
+ {
817
+ method: 'GET',
818
+ headers: this._buildHeaders(),
819
+ },
820
+ );
741
821
 
742
822
  if (!response.ok) {
743
823
  this.errors = { ...this.errors, [type]: await parseResponseError(response, type) };
@@ -805,16 +885,19 @@ const baseActions = {
805
885
 
806
886
  try {
807
887
  const isUpdate = !!objectData.id;
808
- const url = isUpdate
809
- ? this._buildUrl(type, objectData.id)
810
- : this._buildUrl(type);
811
888
  const method = isUpdate ? 'PUT' : 'POST';
812
889
 
813
- const response = await fetch(url, {
814
- method,
815
- headers: this._buildHeaders(),
816
- body: JSON.stringify(objectData),
817
- });
890
+ const response = await this._fetchWithSchemaFallback(
891
+ type,
892
+ (schema) => (isUpdate
893
+ ? this._buildUrl(type, objectData.id, schema)
894
+ : this._buildUrl(type, null, schema)),
895
+ {
896
+ method,
897
+ headers: this._buildHeaders(),
898
+ body: JSON.stringify(objectData),
899
+ },
900
+ );
818
901
 
819
902
  if (!response.ok) {
820
903
  this.errors = { ...this.errors, [type]: await parseResponseError(response, type) };
@@ -866,12 +949,14 @@ const baseActions = {
866
949
  this.errors = { ...this.errors, [type]: null };
867
950
 
868
951
  try {
869
- const url = this._buildUrl(type, id);
870
-
871
- const response = await fetch(url, {
872
- method: 'DELETE',
873
- headers: this._buildHeaders(),
874
- });
952
+ const response = await this._fetchWithSchemaFallback(
953
+ type,
954
+ (schema) => this._buildUrl(type, id, schema),
955
+ {
956
+ method: 'DELETE',
957
+ headers: this._buildHeaders(),
958
+ },
959
+ );
875
960
 
876
961
  if (!response.ok) {
877
962
  this.errors = { ...this.errors, [type]: await parseResponseError(response, type) };