@conduction/nextcloud-vue 2.3.0-beta.1 → 2.3.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,120 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+
6
+ <template>
7
+ <section :class="sectionClass" :style="sectionStyle">
8
+ <div class="container">
9
+ <slot />
10
+ </div>
11
+ </section>
12
+ </template>
13
+
14
+ <script>
15
+ /**
16
+ * A full-bleed band with a constrained reading column inside it.
17
+ *
18
+ * The band paints edge to edge and the `.container` holds the content at the
19
+ * design system's column width. That split is not cosmetic: every band in the
20
+ * NL Design System reference works this way, and a region that emits its
21
+ * content WITHOUT a container renders body copy hard against the viewport edge
22
+ * while the navigation above and the footer below start inset.
23
+ *
24
+ * PUBLIC-SAFE (see ../index.js): no `@nextcloud/*` import, directly or
25
+ * transitively. This component must render at a public origin where there is
26
+ * no Nextcloud, no `OC` global and no translation bundle.
27
+ */
28
+ export default {
29
+ name: 'CnSiteSection',
30
+
31
+ props: {
32
+ /**
33
+ * Vertical rhythm. `spacing` adds the design system's block padding;
34
+ * `hero` is the taller leading band.
35
+ */
36
+ variant: {
37
+ type: String,
38
+ default: 'spacing',
39
+ validator: (v) => ['spacing', 'hero', 'flush'].includes(v),
40
+ },
41
+
42
+ /** Optional background image URL, layered over the band's colour. */
43
+ backgroundImage: {
44
+ type: String,
45
+ default: '',
46
+ },
47
+ },
48
+
49
+ computed: {
50
+ /**
51
+ * Inline background, when the host supplies an image.
52
+ *
53
+ * Inline rather than a class because the URL is per-portal CONTENT; a
54
+ * stylesheet cannot enumerate them. `background-image` is layered OVER
55
+ * the band's own colour rather than replacing it, so a slow or blocked
56
+ * image leaves the blue behind it and the text stays readable — an
57
+ * image that fails is not allowed to become white-on-white.
58
+ *
59
+ * @return {object|null} Style bindings, or null.
60
+ */
61
+ sectionStyle() {
62
+ if (this.backgroundImage === '') {
63
+ return null
64
+ }
65
+
66
+ return {
67
+ backgroundImage: `url(${JSON.stringify(this.backgroundImage)})`,
68
+ backgroundSize: 'cover',
69
+ backgroundPosition: '50% 50%',
70
+ backgroundRepeat: 'no-repeat',
71
+ }
72
+ },
73
+
74
+ /**
75
+ * @return {Array} The band's classes.
76
+ */
77
+ sectionClass() {
78
+ return [
79
+ 'ac-section',
80
+ this.variant === 'hero' ? 'ac-hero' : null,
81
+ this.variant === 'spacing' ? 'ac-section--spacing' : null,
82
+ ].filter(Boolean)
83
+ },
84
+ },
85
+ }
86
+ </script>
87
+
88
+ <style scoped>
89
+ /*
90
+ * ONLY THE TEXT THAT ACTUALLY SITS ON THE BAND IS RECOLOURED.
91
+ *
92
+ * `.ac-hero` paints itself `--tilburg-color-blue-500` — dark — and defines no
93
+ * colour for text placed directly on it, because the reference implementation
94
+ * never puts any there: its hero holds a white input and a blue button, both
95
+ * of which bring their own surface. So a heading or paragraph dropped onto the
96
+ * band inherits the document's dark body colour and lands dark-on-dark.
97
+ *
98
+ * THIS RULE WAS FIRST WRITTEN ON `.ac-hero` ITSELF, AND THAT WAS WRONG — it is
99
+ * the very mistake the rest of this codebase keeps recording. Colour set on an
100
+ * ancestor cannot know that a descendant paints its own background:
101
+ * `.ac-search-box` has one, `rgb(230, 246, 255)`, and inheriting white into it
102
+ * put the label and button at contrast 1.11 and the input's text at 1.0.
103
+ *
104
+ * Worse, the "bug" that prompted it was a MEASUREMENT ERROR. The label
105
+ * computed `rgb(51, 51, 51)`, which was compared against the BAND and scored
106
+ * 1.06 — but the label does not sit on the band, it sits on the search box's
107
+ * own light surface, where that colour is entirely correct. The contrast probe
108
+ * has to walk up to the first ancestor that actually paints a background;
109
+ * against the nearest NAMED band it reports a failure that is not there, and
110
+ * then invites a fix that breaks something real.
111
+ *
112
+ * So: the hero's own title and subtitle, and nothing else. The system's
113
+ * inverse token rather than a literal, so a theme with a light hero corrects
114
+ * both together.
115
+ */
116
+ .ac-hero__title,
117
+ .ac-hero__subtitle {
118
+ color: var(--utrecht-document-inverse-color, #ffffff);
119
+ }
120
+ </style>
@@ -0,0 +1,130 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * PUBLIC-SAFE SITE BLOCKS — the entry point a portal renderer can import when
6
+ * it runs at a public origin with NO Nextcloud behind it.
7
+ *
8
+ * WHY THIS ENTRY POINT EXISTS AT ALL
9
+ *
10
+ * nc-vue's main entry is deeply coupled to the Nextcloud runtime: measured on
11
+ * the published build, 154 files reference `@nextcloud/axios` and 157
12
+ * reference `@nextcloud/router`. That is correct for an app rendering inside
13
+ * Nextcloud and fatal for one rendering at `https://portaal.gemeente.nl`,
14
+ * where there is no `OC` global, no session and no translation bundle.
15
+ *
16
+ * WHY THESE ARE NEW COMPONENTS RATHER THAN RE-EXPORTS
17
+ *
18
+ * The first plan was to re-export the existing widgets that "looked clean".
19
+ * A DIRECT import check agreed: 12 of 13 candidates had no `@nextcloud/*`
20
+ * import in their own file. A TRANSITIVE check — following relative imports
21
+ * through the tree — inverted that result completely:
22
+ *
23
+ * CnTextWidget -> @nextcloud/l10n
24
+ * CnHeaderWidget -> @nextcloud/l10n, @nextcloud/router
25
+ * CnCardGrid -> @nextcloud/l10n, @nextcloud/vue,
26
+ * @nextcloud/auth, @nextcloud/event-bus
27
+ * ... 12 of 13 unsafe
28
+ *
29
+ * Only `CnCard` was clean all the way down. Re-exporting the rest under a
30
+ * name promising public safety would have shipped a guarantee that the first
31
+ * public deployment disproved — and it would have failed at RUNTIME, in a
32
+ * browser, on a government portal, rather than at build time here.
33
+ *
34
+ * So these blocks take their strings as PROPS instead of calling `t()`. That
35
+ * single decision is what keeps them portable: `@nextcloud/l10n` is the
36
+ * dependency almost every existing widget trips over.
37
+ *
38
+ * THE GUARANTEE IS CHECKED, NOT ASSERTED. `npm run check:public-safe` walks
39
+ * this entry's transitive imports and fails on any `@nextcloud/*`. A comment
40
+ * claiming purity is worth nothing; the check is in CI.
41
+ */
42
+
43
+ import CnSiteCard from './components/CnSiteCard.vue'
44
+ import CnSiteCardGrid from './components/CnSiteCardGrid.vue'
45
+ import CnSiteEmptyState from './components/CnSiteEmptyState.vue'
46
+ import CnSiteHero from './components/CnSiteHero.vue'
47
+ import CnSiteIcon from './components/CnSiteIcon.vue'
48
+ import CnSiteSearch from './components/CnSiteSearch.vue'
49
+ import CnSiteSection from './components/CnSiteSection.vue'
50
+
51
+ export {
52
+ CnSiteCard,
53
+ CnSiteCardGrid,
54
+ CnSiteEmptyState,
55
+ CnSiteHero,
56
+ CnSiteIcon,
57
+ CnSiteSearch,
58
+ CnSiteSection,
59
+ }
60
+
61
+ /**
62
+ * The block vocabulary, keyed by the `widgetKey` a page body declares.
63
+ *
64
+ * A REGISTRY rather than a switch in the consumer. A consumer that resolves
65
+ * blocks with `if (key === 'markdown')` cannot be extended without editing the
66
+ * consumer, which is how portaliq ended up supporting exactly one block type
67
+ * while this library already shipped thirty-four.
68
+ *
69
+ * Anything not in this map is UNKNOWN, and a renderer must say so visibly
70
+ * rather than render nothing — a page silently missing a block looks identical
71
+ * to a page that was authored empty.
72
+ *
73
+ * @type {Record<string, object>}
74
+ */
75
+ export const siteBlockRegistry = {
76
+ hero: CnSiteHero,
77
+ search: CnSiteSearch,
78
+ section: CnSiteSection,
79
+ cardGrid: CnSiteCardGrid,
80
+ card: CnSiteCard,
81
+ emptyState: CnSiteEmptyState,
82
+ }
83
+
84
+ /**
85
+ * Blocks that are FULL-BLEED BANDS and carry their own container.
86
+ *
87
+ * A band paints edge to edge and constrains its content itself. Rendering one
88
+ * inside a host's content column silently shrinks it: measured against the NL
89
+ * Design System reference, a hero nested in the page container came out
90
+ * 1168px wide against the design's 1280, and no amount of styling inside the
91
+ * hero could recover the missing width because the clamp was an ancestor.
92
+ *
93
+ * The reference's own structure is the model — `main` is full-bleed and every
94
+ * `section` brings a `.container` — so a host must ask this before deciding
95
+ * whether to wrap a block.
96
+ *
97
+ * @param {string} key The block key.
98
+ * @return {boolean} True when the block must NOT be wrapped in a container.
99
+ */
100
+ export function siteBlockIsBand(key) {
101
+ return SITE_BAND_BLOCKS.includes(key)
102
+ }
103
+
104
+ /**
105
+ * @type {Array<string>} The band block keys.
106
+ */
107
+ export const SITE_BAND_BLOCKS = ['hero', 'section']
108
+
109
+ /**
110
+ * Resolve a block component by key.
111
+ *
112
+ * @param {string} key The `widgetKey` from a page body.
113
+ * @return {object|null} The component, or null when the key is unknown.
114
+ */
115
+ export function siteBlockFor(key) {
116
+ return siteBlockRegistry[key] || null
117
+ }
118
+
119
+ /**
120
+ * The keys this entry point can render.
121
+ *
122
+ * Exported so a consumer — or a test — can assert the vocabulary it depends on
123
+ * is actually present, rather than discovering a missing block as a blank area
124
+ * on a live page.
125
+ *
126
+ * @return {Array<string>} The known block keys.
127
+ */
128
+ export function listSiteBlocks() {
129
+ return Object.keys(siteBlockRegistry)
130
+ }