@conduction/docusaurus-preset 3.27.2 → 3.31.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 (38) hide show
  1. package/README.md +34 -0
  2. package/package.json +6 -5
  3. package/src/components/AiDisclosure/AiDisclosure.jsx +82 -0
  4. package/src/components/AiDisclosure/AiDisclosure.module.css +35 -0
  5. package/src/components/AiDisclosure/__tests__/AiDisclosure.render.test.js +127 -0
  6. package/src/components/AiDisclosure/__tests__/disclosure.test.js +218 -0
  7. package/src/components/AiDisclosure/disclosure.js +169 -0
  8. package/src/components/AppMock/AppMock.jsx +16 -0
  9. package/src/components/AppMock/variants/AppVersionsMock.jsx +83 -0
  10. package/src/components/AppMock/variants/DoriathMock.jsx +79 -0
  11. package/src/components/AppMock/variants/HermiqMock.jsx +71 -0
  12. package/src/components/AppMock/variants/HrmqMock.jsx +77 -0
  13. package/src/components/AppMock/variants/PlanixMock.jsx +80 -0
  14. package/src/components/AppMock/variants/PortaliqMock.jsx +63 -0
  15. package/src/components/AppMock/variants/ScholiqMock.jsx +74 -0
  16. package/src/components/AppMock/variants/ShillinqMock.jsx +90 -0
  17. package/src/components/CookieCli/CookieCli.jsx +197 -24
  18. package/src/components/CookieCli/CookieCli.module.css +139 -1
  19. package/src/components/CookieCli/shell.js +161 -0
  20. package/src/components/CookieCli/spaceInvaders.js +359 -0
  21. package/src/components/CookieCli/spaceInvaders.test.js +112 -0
  22. package/src/components/index.js +7 -0
  23. package/src/data/app-glyphs.json +4 -0
  24. package/src/theme/BlogPostItem/Content/index.jsx +43 -0
  25. package/src/theme/DocItem/Content/index.jsx +15 -0
  26. package/static/img/ai-disclosure/PROVENANCE.md +87 -0
  27. package/static/img/ai-disclosure/ai-black-transparent.svg +20 -0
  28. package/static/img/ai-disclosure/ai-black.svg +19 -0
  29. package/static/img/ai-disclosure/ai-generated-black-transparent.svg +31 -0
  30. package/static/img/ai-disclosure/ai-generated-black.svg +30 -0
  31. package/static/img/ai-disclosure/ai-generated-white-transparent.svg +32 -0
  32. package/static/img/ai-disclosure/ai-generated-white.svg +31 -0
  33. package/static/img/ai-disclosure/ai-modified-black-transparent.svg +30 -0
  34. package/static/img/ai-disclosure/ai-modified-black.svg +29 -0
  35. package/static/img/ai-disclosure/ai-modified-white-transparent.svg +31 -0
  36. package/static/img/ai-disclosure/ai-modified-white.svg +30 -0
  37. package/static/img/ai-disclosure/ai-white-transparent.svg +21 -0
  38. package/static/img/ai-disclosure/ai-white.svg +20 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * disclosure.js — pure resolver + copy/asset tables for AiDisclosure.
3
+ *
4
+ * No React and no webpack-only imports (no `?url` SVG imports) live
5
+ * here on purpose: this module is `require()`-able directly by
6
+ * Node's built-in test runner, and `import`-able from the JSX
7
+ * component via CommonJS/ESM interop. Keeping the decision logic and
8
+ * the copy register out of the component is what makes tasks 5.1-5.4
9
+ * testable without spinning up a bundler.
10
+ *
11
+ * @spec openspec/changes/ai-content-disclosure/tasks.md#task-1.2
12
+ * @spec openspec/changes/ai-content-disclosure/tasks.md#task-2.1
13
+ * @spec openspec/changes/ai-content-disclosure/tasks.md#task-2.2
14
+ * @spec openspec/changes/ai-content-disclosure/tasks.md#task-3.3
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ // The three values the `ai` frontmatter key accepts. Anything else -
20
+ // including an empty string - is an unrecognised value per the spec's
21
+ // "An unrecognised value fails loudly and renders nothing" requirement.
22
+ const AI_KINDS = ['generated', 'modified', 'assisted'];
23
+
24
+ // kind -> vendored icon filename prefix (see static/img/ai-disclosure/
25
+ // PROVENANCE.md). "assisted" maps to the Commission's Basic mark,
26
+ // whose vendored files carry no kind segment in the filename
27
+ // (`ai-black.svg`, not `ai-basic-black.svg`).
28
+ const KIND_TO_FILE_PREFIX = {
29
+ generated: 'ai-generated',
30
+ modified: 'ai-modified',
31
+ assisted: 'ai',
32
+ };
33
+
34
+ const TREATMENTS = ['black', 'white', 'black-transparent', 'white-transparent'];
35
+
36
+ // Source viewBox dimensions per mark (icons/PROVENANCE.md). Each mark
37
+ // has its own aspect ratio; a single uniform box would stretch two of
38
+ // the three marks.
39
+ const VIEWBOX = {
40
+ assisted: {width: 566.93, height: 566.93},
41
+ modified: {width: 1700.79, height: 566.93},
42
+ generated: {width: 1789.84, height: 566.93},
43
+ };
44
+
45
+ // Three kinds x four locales. Every string states a fact about the
46
+ // page and stops - no compliance claim, no Code-of-Practice claim.
47
+ // See design.md "The thing most likely to be got wrong".
48
+ const COPY = {
49
+ nl: {
50
+ generated: 'Deze pagina is gegenereerd met AI.',
51
+ modified: 'Deze pagina is gedeeltelijk aangepast met AI.',
52
+ assisted: 'Deze pagina is geschreven met hulp van AI.',
53
+ },
54
+ en: {
55
+ generated: 'This page was generated with AI.',
56
+ modified: 'This page was partially modified with AI.',
57
+ assisted: 'This page was written with AI assistance.',
58
+ },
59
+ de: {
60
+ generated: 'Diese Seite wurde mit KI erstellt.',
61
+ modified: 'Diese Seite wurde teilweise mit KI bearbeitet.',
62
+ assisted: 'Diese Seite wurde mit KI-Unterstützung geschrieben.',
63
+ },
64
+ fr: {
65
+ generated: "Cette page a été générée avec l'IA.",
66
+ modified: "Cette page a été partiellement modifiée avec l'IA.",
67
+ assisted: "Cette page a été rédigée avec l'aide de l'IA.",
68
+ },
69
+ };
70
+
71
+ const LOCALES = Object.keys(COPY);
72
+
73
+ function isValidKind(value) {
74
+ return AI_KINDS.indexOf(value) !== -1;
75
+ }
76
+
77
+ /**
78
+ * Resolve a page's `ai` frontmatter value.
79
+ *
80
+ * Absence (`undefined`) is the normal opt-out case: no banner, no
81
+ * warning. A *present* but unrecognised value (unknown string, empty
82
+ * string) is a defect - it warns, naming the file, the offending
83
+ * value, and the permitted values, and still resolves to no kind. It
84
+ * never falls back to a mark: a typo must not become a published
85
+ * authorship claim.
86
+ *
87
+ * @param {string|undefined} rawValue - the page's frontmatter `ai` value
88
+ * @param {string} sourcePath - the doc/blog source file, for the warning
89
+ * @returns {{kind: string|null, warning: string|null}}
90
+ */
91
+ function resolveAiFrontmatter(rawValue, sourcePath) {
92
+ if (rawValue === undefined) {
93
+ return {kind: null, warning: null};
94
+ }
95
+ if (isValidKind(rawValue)) {
96
+ return {kind: rawValue, warning: null};
97
+ }
98
+ const file = sourcePath || 'unknown file';
99
+ const shown = rawValue === '' ? '(empty)' : JSON.stringify(rawValue);
100
+ const warning =
101
+ `[ai-content-disclosure] ${file}: unrecognised "ai" frontmatter value ${shown} - ` +
102
+ `permitted values are ${AI_KINDS.join(', ')}. Rendering no disclosure banner.`;
103
+ return {kind: null, warning};
104
+ }
105
+
106
+ function getIconFilename(kind, treatment) {
107
+ if (!isValidKind(kind)) {
108
+ throw new Error(`getIconFilename: unknown kind "${kind}"`);
109
+ }
110
+ if (TREATMENTS.indexOf(treatment) === -1) {
111
+ throw new Error(`getIconFilename: unknown treatment "${treatment}"`);
112
+ }
113
+ return `${KIND_TO_FILE_PREFIX[kind]}-${treatment}.svg`;
114
+ }
115
+
116
+ // Site-relative (no leading slash) path under `static/`. The marks are
117
+ // served as plain static files - not run through webpack's `.svg`
118
+ // pipeline - because this preset also enables @docusaurus/plugin-svgr
119
+ // (via the classic preset), which unconditionally turns any `.svg`
120
+ // imported from a JS/JSX/MDX file into a React component, regardless
121
+ // of a `?url` suffix. An inlined SVGR component would also reuse
122
+ // unscoped `.cls-N { fill: ... }` class names across every mark on the
123
+ // page (see static/img/ai-disclosure/PROVENANCE.md), so two different
124
+ // marks rendered together could bleed colours into each other. A
125
+ // plain `<img src>` pointed at an untouched static file avoids both
126
+ // problems and is the same mechanism this preset already uses for the
127
+ // favicon and the default og:image (see static/img/).
128
+ const ICONS_BASE_PATH = 'img/ai-disclosure';
129
+
130
+ function getIconPath(kind, treatment) {
131
+ return `${ICONS_BASE_PATH}/${getIconFilename(kind, treatment)}`;
132
+ }
133
+
134
+ function getViewBox(kind) {
135
+ if (!isValidKind(kind)) {
136
+ throw new Error(`getViewBox: unknown kind "${kind}"`);
137
+ }
138
+ return VIEWBOX[kind];
139
+ }
140
+
141
+ /**
142
+ * Copy for one kind in one locale. No fallback: every locale in
143
+ * LOCALES must carry all three kinds (asserted by the locale-coverage
144
+ * test), so an undefined return here means the COPY table itself is
145
+ * incomplete, not that a reader silently sees a leaked English string.
146
+ */
147
+ function getCopy(kind, locale) {
148
+ if (!isValidKind(kind)) {
149
+ throw new Error(`getCopy: unknown kind "${kind}"`);
150
+ }
151
+ const table = COPY[locale];
152
+ return table ? table[kind] : undefined;
153
+ }
154
+
155
+ module.exports = {
156
+ AI_KINDS,
157
+ TREATMENTS,
158
+ LOCALES,
159
+ KIND_TO_FILE_PREFIX,
160
+ VIEWBOX,
161
+ COPY,
162
+ ICONS_BASE_PATH,
163
+ isValidKind,
164
+ resolveAiFrontmatter,
165
+ getIconFilename,
166
+ getIconPath,
167
+ getViewBox,
168
+ getCopy,
169
+ };
@@ -51,6 +51,14 @@ import OpenWooMock from './variants/OpenWooMock.jsx';
51
51
  import PipelinQMock from './variants/PipelinQMock.jsx';
52
52
  import SoftwareCatalogMock from './variants/SoftwareCatalogMock.jsx';
53
53
  import ZaakAfhandelAppMock from './variants/ZaakAfhandelAppMock.jsx';
54
+ import HermiqMock from './variants/HermiqMock.jsx';
55
+ import PortaliqMock from './variants/PortaliqMock.jsx';
56
+ import ScholiqMock from './variants/ScholiqMock.jsx';
57
+ import ShillinqMock from './variants/ShillinqMock.jsx';
58
+ import DoriathMock from './variants/DoriathMock.jsx';
59
+ import PlanixMock from './variants/PlanixMock.jsx';
60
+ import HrmqMock from './variants/HrmqMock.jsx';
61
+ import AppVersionsMock from './variants/AppVersionsMock.jsx';
54
62
 
55
63
  const VARIANTS = {
56
64
  launchpad: {Component: LaunchPadMock, label: 'LaunchPad'},
@@ -70,6 +78,14 @@ const VARIANTS = {
70
78
  pipelinq: {Component: PipelinQMock, label: 'PipelinQ'},
71
79
  softwarecatalog: {Component: SoftwareCatalogMock, label: 'SoftwareCatalog'},
72
80
  zaakafhandelapp: {Component: ZaakAfhandelAppMock, label: 'ZaakAfhandelApp'},
81
+ hermiq: {Component: HermiqMock, label: 'Hermiq'},
82
+ portaliq: {Component: PortaliqMock, label: 'Portaliq'},
83
+ scholiq: {Component: ScholiqMock, label: 'Scholiq'},
84
+ shillinq: {Component: ShillinqMock, label: 'Shillinq'},
85
+ doriath: {Component: DoriathMock, label: 'Doriath'},
86
+ planix: {Component: PlanixMock, label: 'Planix'},
87
+ hrmq: {Component: HrmqMock, label: 'HRMQ'},
88
+ 'app-versions': {Component: AppVersionsMock, label: 'App Versions'},
73
89
  };
74
90
 
75
91
  export default function AppMock({app, size = 'md', sidebar = null, caption = false, className}) {
@@ -0,0 +1,83 @@
1
+ /**
2
+ * App Versions abstract — fleet version dashboard.
3
+ *
4
+ * Inferred from the app role (tracking installed app versions across
5
+ * a workspace): a KPI strip over an app list — app hex, name, version
6
+ * chip, and a status dot column where mint = up to date and
7
+ * terracotta = an update is waiting. Tone: workspace-blue — this is
8
+ * about the workspace itself.
9
+ */
10
+
11
+ import React from 'react';
12
+ import styles from '../AppMock.module.css';
13
+
14
+ const APPS = [
15
+ {upToDate: true},
16
+ {upToDate: true},
17
+ {upToDate: false, newChip: true},
18
+ {upToDate: true},
19
+ {upToDate: false, newChip: true},
20
+ {upToDate: true},
21
+ ];
22
+
23
+ export default function AppVersionsMock() {
24
+ return (
25
+ <>
26
+ <div className={styles.topbar}>
27
+ <div className={styles.logo}></div>
28
+ {Array.from({length: 14}).map((_, i) => <div key={i} className={styles.icon}></div>)}
29
+ <div className={styles.spacer}></div>
30
+ <div className={styles.bell}></div>
31
+ <div className={styles.avatar}></div>
32
+ </div>
33
+ <div className={[styles.body, styles.decidesk].filter(Boolean).join(' ')}>
34
+ <div className={styles.nav}>
35
+ <div className={styles.navHead}><div className={styles.h}></div><div className={styles.l}></div></div>
36
+ {[true, false, false].map((active, i) => (
37
+ <div key={i} className={[styles.item, active && styles.active].filter(Boolean).join(' ')}>
38
+ <div className={styles.ico}></div>
39
+ <div className={styles.l}></div>
40
+ </div>
41
+ ))}
42
+ </div>
43
+ <div className={styles.col}>
44
+ <div className={styles.head}>
45
+ <div className={styles.row + ' ' + styles.head} style={{width: '30%'}}></div>
46
+ <div className={styles.actions}>
47
+ <div className={styles.btn + ' ' + styles.ghost}></div>
48
+ </div>
49
+ </div>
50
+ <div className={styles.kpiRow}>
51
+ <div className={styles.kpi}>
52
+ <div className={styles.ico} style={{background: 'var(--c-workspaceblue-500)'}}></div>
53
+ <div className={styles.meta}><div className={styles.num}></div><div className={styles.label}></div></div>
54
+ </div>
55
+ <div className={styles.kpi}>
56
+ <div className={styles.ico}></div>
57
+ <div className={styles.meta}><div className={styles.num}></div><div className={styles.label}></div></div>
58
+ </div>
59
+ <div className={styles.kpi}>
60
+ <div className={styles.ico} style={{background: 'var(--c-terracotta-300)'}}></div>
61
+ <div className={styles.meta}><div className={styles.num}></div><div className={styles.label}></div></div>
62
+ </div>
63
+ </div>
64
+ {/* App rows: hex · name · version chip(s) · status dot */}
65
+ <div className={styles.panel} style={{flex: 1}}>
66
+ <div className={styles.head}><div className={styles.title}></div></div>
67
+ <div className={styles.stack}>
68
+ {APPS.map(({upToDate, newChip}, i) => (
69
+ <div key={i} className={styles.item}>
70
+ <div style={{width: 11, height: 13, clipPath: 'var(--hex-pointy-top)', background: upToDate ? 'var(--c-workspaceblue-500)' : 'var(--c-workspaceblue-300)', flexShrink: 0}}></div>
71
+ <div className={styles.lines}><div className={styles.l1}></div></div>
72
+ <div style={{width: 26, height: 9, borderRadius: 999, background: 'var(--c-cobalt-100)', flexShrink: 0}}></div>
73
+ {newChip && <div style={{width: 26, height: 9, borderRadius: 999, background: 'var(--c-workspaceblue-300)', flexShrink: 0}}></div>}
74
+ <div style={{width: 6, height: 6, borderRadius: '50%', background: upToDate ? 'var(--c-mint-500)' : 'var(--c-terracotta-500)', flexShrink: 0}}></div>
75
+ </div>
76
+ ))}
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </>
82
+ );
83
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Doriath abstract — secrets vault.
3
+ *
4
+ * Inferred from the app role (storing and sharing secrets): a list of
5
+ * locked entries — key hex, name, masked value rendered as a dot
6
+ * pattern — next to one unlocked detail card on a cobalt-900 surface
7
+ * with the revealed value in light. Tones: cobalt-900 for the vault
8
+ * interior, lavender for the keys.
9
+ */
10
+
11
+ import React from 'react';
12
+ import styles from '../AppMock.module.css';
13
+
14
+ export default function DoriathMock() {
15
+ const mask = (n) => (
16
+ <div style={{display: 'flex', gap: 2, flexShrink: 0}}>
17
+ {Array.from({length: n}).map((_, i) => (
18
+ <div key={i} style={{width: 3, height: 3, borderRadius: '50%', background: 'var(--c-cobalt-400)'}}></div>
19
+ ))}
20
+ </div>
21
+ );
22
+ return (
23
+ <>
24
+ <div className={styles.topbar}>
25
+ <div className={styles.logo}></div>
26
+ {Array.from({length: 14}).map((_, i) => <div key={i} className={styles.icon}></div>)}
27
+ <div className={styles.spacer}></div>
28
+ <div className={styles.bell}></div>
29
+ <div className={styles.avatar}></div>
30
+ </div>
31
+ <div className={[styles.body, styles.decidesk].filter(Boolean).join(' ')}>
32
+ <div className={styles.nav}>
33
+ <div className={styles.navHead}><div className={styles.h}></div><div className={styles.l}></div></div>
34
+ {[true, false, false, false].map((active, i) => (
35
+ <div key={i} className={[styles.item, active && styles.active].filter(Boolean).join(' ')}>
36
+ <div className={styles.ico}></div>
37
+ <div className={styles.l}></div>
38
+ </div>
39
+ ))}
40
+ </div>
41
+ <div className={styles.col}>
42
+ <div className={styles.head}>
43
+ <div className={styles.row + ' ' + styles.head} style={{width: '25%'}}></div>
44
+ <div className={styles.actions}>
45
+ <div className={styles.btn + ' ' + styles.ghost}></div>
46
+ <div className={styles.btn}></div>
47
+ </div>
48
+ </div>
49
+ <div className={styles.panelRow} style={{gridTemplateColumns: '1.4fr 1fr'}}>
50
+ {/* Locked entries — key hex + name + masked value */}
51
+ <div className={styles.panel}>
52
+ <div className={styles.head}><div className={styles.title}></div></div>
53
+ <div className={styles.stack}>
54
+ {[6, 5, 7, 5, 6].map((dots, i) => (
55
+ <div key={i} className={styles.item}>
56
+ <div style={{width: 10, height: 11, clipPath: 'var(--hex-pointy-top)', background: 'var(--c-lavender-500)', flexShrink: 0}}></div>
57
+ <div className={styles.lines}><div className={styles.l1}></div></div>
58
+ {mask(dots)}
59
+ </div>
60
+ ))}
61
+ </div>
62
+ </div>
63
+ {/* Unlocked entry — vault-dark card, value revealed */}
64
+ <div className={styles.panel} style={{background: 'var(--c-cobalt-900)', borderColor: 'var(--c-cobalt-900)'}}>
65
+ <div style={{display: 'flex', alignItems: 'center', gap: 5}}>
66
+ <div style={{width: 12, height: 14, clipPath: 'var(--hex-pointy-top)', background: 'var(--c-lavender-300)', flexShrink: 0}}></div>
67
+ <div style={{height: 6, width: '45%', background: 'var(--c-lavender-300)', borderRadius: 1}}></div>
68
+ </div>
69
+ <div style={{height: 3, width: '60%', background: 'rgba(255,255,255,0.25)', borderRadius: 1}}></div>
70
+ <div style={{height: 10, width: '85%', background: 'rgba(255,255,255,0.9)', borderRadius: 2}}></div>
71
+ <div style={{height: 3, width: '50%', background: 'rgba(255,255,255,0.25)', borderRadius: 1}}></div>
72
+ <div style={{height: 3, width: '70%', background: 'rgba(255,255,255,0.25)', borderRadius: 1}}></div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </>
78
+ );
79
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Hermiq abstract — agent chat surface.
3
+ *
4
+ * Inferred from the app role (AI agents working inside the workspace):
5
+ * left rail lists the available agents, centre shows a conversation —
6
+ * user bubbles right, agent bubbles left — with a tool-call chip row
7
+ * between agent turns and a prompt input at the bottom. Tones:
8
+ * workspace-blue for the human side, mint for confirmed tool runs.
9
+ */
10
+
11
+ import React from 'react';
12
+ import styles from '../AppMock.module.css';
13
+
14
+ export default function HermiqMock() {
15
+ return (
16
+ <>
17
+ <div className={styles.topbar}>
18
+ <div className={styles.logo}></div>
19
+ {Array.from({length: 14}).map((_, i) => <div key={i} className={styles.icon}></div>)}
20
+ <div className={styles.spacer}></div>
21
+ <div className={styles.bell}></div>
22
+ <div className={styles.avatar}></div>
23
+ </div>
24
+ <div className={[styles.body, styles.decidesk].filter(Boolean).join(' ')}>
25
+ {/* Agent list rail */}
26
+ <div className={styles.nav}>
27
+ <div className={styles.navHead}><div className={styles.h}></div><div className={styles.l}></div></div>
28
+ {[true, false, false, false].map((active, i) => (
29
+ <div key={i} className={[styles.item, active && styles.active].filter(Boolean).join(' ')}>
30
+ <div className={styles.ico} style={{borderRadius: '50%'}}></div>
31
+ <div className={styles.l}></div>
32
+ </div>
33
+ ))}
34
+ </div>
35
+ {/* Conversation */}
36
+ <div className={styles.col}>
37
+ <div className={styles.head}>
38
+ <div className={styles.row + ' ' + styles.head} style={{width: '30%'}}></div>
39
+ <div className={styles.actions}>
40
+ <div className={styles.btn + ' ' + styles.ghost}></div>
41
+ </div>
42
+ </div>
43
+ <div className={styles.panel} style={{flex: 1, gap: 8, justifyContent: 'flex-end'}}>
44
+ {/* User asks */}
45
+ <div style={{alignSelf: 'flex-end', width: '42%', height: 12, borderRadius: '8px 2px 8px 8px', background: 'var(--c-workspaceblue-300)'}}></div>
46
+ {/* Agent answers */}
47
+ <div style={{alignSelf: 'flex-start', width: '58%', height: 18, borderRadius: '2px 8px 8px 8px', background: 'var(--c-cobalt-50)', border: '1px solid var(--c-cobalt-100)'}}></div>
48
+ {/* Tool-call chip row — mint = tool run confirmed */}
49
+ <div style={{display: 'flex', alignItems: 'center', gap: 4}}>
50
+ <div className={styles.statusPill}>
51
+ <div className={styles.h}></div>
52
+ <div className={styles.t}></div>
53
+ </div>
54
+ <div style={{width: 34, height: 9, borderRadius: 999, background: 'var(--c-cobalt-100)'}}></div>
55
+ <div style={{width: 26, height: 9, borderRadius: 999, background: 'var(--c-cobalt-100)'}}></div>
56
+ </div>
57
+ {/* Agent continues with the tool result */}
58
+ <div style={{alignSelf: 'flex-start', width: '64%', height: 24, borderRadius: '2px 8px 8px 8px', background: 'var(--c-cobalt-50)', border: '1px solid var(--c-cobalt-100)'}}></div>
59
+ {/* User follows up */}
60
+ <div style={{alignSelf: 'flex-end', width: '34%', height: 12, borderRadius: '8px 2px 8px 8px', background: 'var(--c-workspaceblue-300)'}}></div>
61
+ </div>
62
+ {/* Prompt input */}
63
+ <div className={styles.actions} style={{alignItems: 'center'}}>
64
+ <div style={{flex: 1, height: 16, background: 'white', border: '1px solid var(--c-cobalt-200)', borderRadius: 8}}></div>
65
+ <div className={styles.btn} style={{width: 28}}></div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </>
70
+ );
71
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * HRMQ abstract — people administration.
3
+ *
4
+ * Inferred from the app role (HR: employees, contracts, leave): a
5
+ * people list with hex avatars next to a leave-balance card of
6
+ * labelled horizontal bars. Tone: mint — the team is in order.
7
+ */
8
+
9
+ import React from 'react';
10
+ import styles from '../AppMock.module.css';
11
+
12
+ const PEOPLE = ['var(--c-mint-500)', 'var(--c-mint-300)', 'var(--c-forest-300)', 'var(--c-mint-300)', 'var(--c-mint-500)'];
13
+ const BALANCES = [
14
+ {fill: '72%', shade: 'var(--c-mint-500)'},
15
+ {fill: '45%', shade: 'var(--c-mint-500)'},
16
+ {fill: '88%', shade: 'var(--c-mint-300)'},
17
+ {fill: '20%', shade: 'var(--c-mint-300)'},
18
+ ];
19
+
20
+ export default function HrmqMock() {
21
+ return (
22
+ <>
23
+ <div className={styles.topbar}>
24
+ <div className={styles.logo}></div>
25
+ {Array.from({length: 14}).map((_, i) => <div key={i} className={styles.icon}></div>)}
26
+ <div className={styles.spacer}></div>
27
+ <div className={styles.bell}></div>
28
+ <div className={styles.avatar}></div>
29
+ </div>
30
+ <div className={[styles.body, styles.decidesk].filter(Boolean).join(' ')}>
31
+ <div className={styles.nav}>
32
+ <div className={styles.navHead}><div className={styles.h}></div><div className={styles.l}></div></div>
33
+ {[true, false, false, false, false].map((active, i) => (
34
+ <div key={i} className={[styles.item, active && styles.active].filter(Boolean).join(' ')}>
35
+ <div className={styles.ico}></div>
36
+ <div className={styles.l}></div>
37
+ </div>
38
+ ))}
39
+ </div>
40
+ <div className={styles.col}>
41
+ <div className={styles.head}>
42
+ <div className={styles.row + ' ' + styles.head} style={{width: '25%'}}></div>
43
+ <div className={styles.actions}>
44
+ <div className={styles.btn}></div>
45
+ </div>
46
+ </div>
47
+ <div className={styles.panelRow} style={{gridTemplateColumns: '1.2fr 1fr'}}>
48
+ {/* People list — hex avatars */}
49
+ <div className={styles.panel}>
50
+ <div className={styles.head}><div className={styles.title}></div></div>
51
+ <div className={styles.stack}>
52
+ {PEOPLE.map((shade, i) => (
53
+ <div key={i} className={styles.item}>
54
+ <div style={{width: 13, height: 15, clipPath: 'var(--hex-pointy-top)', background: shade, flexShrink: 0}}></div>
55
+ <div className={styles.lines}><div className={styles.l1}></div><div className={styles.l2}></div></div>
56
+ </div>
57
+ ))}
58
+ </div>
59
+ </div>
60
+ {/* Leave-balance card — labelled bars */}
61
+ <div className={styles.panel}>
62
+ <div className={styles.head}><div className={styles.title}></div></div>
63
+ {BALANCES.map(({fill, shade}, i) => (
64
+ <div key={i} style={{display: 'flex', flexDirection: 'column', gap: 3}}>
65
+ <div className={styles.row + ' ' + styles.short} style={{height: 3}}></div>
66
+ <div style={{height: 5, background: 'var(--c-cobalt-100)', borderRadius: 2}}>
67
+ <div style={{width: fill, height: 5, background: shade, borderRadius: 2}}></div>
68
+ </div>
69
+ </div>
70
+ ))}
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </>
76
+ );
77
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Planix abstract — planning board.
3
+ *
4
+ * Inferred from the app role (scheduling people and work over time):
5
+ * a week grid of five day columns with placed planning blocks at
6
+ * varying heights, over a small capacity KPI strip. Tone: lavender —
7
+ * process and workflow.
8
+ */
9
+
10
+ import React from 'react';
11
+ import styles from '../AppMock.module.css';
12
+
13
+ /* Per day column: [height, shade] pairs; null = free slot spacer. */
14
+ const WEEK = [
15
+ [[14, 500], [10, 300]],
16
+ [[8, 300], [16, 500], [8, 300]],
17
+ [[12, 300], null, [10, 500]],
18
+ [[18, 500]],
19
+ [[8, 300], [8, 300]],
20
+ ];
21
+
22
+ export default function PlanixMock() {
23
+ return (
24
+ <>
25
+ <div className={styles.topbar}>
26
+ <div className={styles.logo}></div>
27
+ {Array.from({length: 14}).map((_, i) => <div key={i} className={styles.icon}></div>)}
28
+ <div className={styles.spacer}></div>
29
+ <div className={styles.bell}></div>
30
+ <div className={styles.avatar}></div>
31
+ </div>
32
+ <div className={[styles.body, styles.decidesk].filter(Boolean).join(' ')}>
33
+ <div className={styles.nav}>
34
+ <div className={styles.navHead}><div className={styles.h}></div><div className={styles.l}></div></div>
35
+ {[true, false, false, false].map((active, i) => (
36
+ <div key={i} className={[styles.item, active && styles.active].filter(Boolean).join(' ')}>
37
+ <div className={styles.ico}></div>
38
+ <div className={styles.l}></div>
39
+ </div>
40
+ ))}
41
+ </div>
42
+ <div className={styles.col}>
43
+ <div className={styles.head}>
44
+ <div className={styles.row + ' ' + styles.head} style={{width: '25%'}}></div>
45
+ <div className={styles.actions}>
46
+ <div className={styles.btn + ' ' + styles.ghost}></div>
47
+ <div className={styles.btn}></div>
48
+ </div>
49
+ </div>
50
+ <div className={styles.kpiRow}>
51
+ <div className={[styles.kpi, styles.lavender].join(' ')}>
52
+ <div className={styles.ico}></div>
53
+ <div className={styles.meta}><div className={styles.num}></div><div className={styles.label}></div></div>
54
+ </div>
55
+ <div className={styles.kpi}>
56
+ <div className={styles.ico}></div>
57
+ <div className={styles.meta}><div className={styles.num}></div><div className={styles.label}></div></div>
58
+ </div>
59
+ </div>
60
+ {/* Week grid with placed planning blocks */}
61
+ <div className={styles.w} style={{flex: 1}}>
62
+ <div className={styles.wHead}>
63
+ <div className={styles.h}></div><div className={styles.t}></div>
64
+ </div>
65
+ <div style={{flex: 1, display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 4, minHeight: 60}}>
66
+ {WEEK.map((day, i) => (
67
+ <div key={i} style={{background: 'var(--c-cobalt-50)', borderRadius: 2, padding: 3, display: 'flex', flexDirection: 'column', gap: 2}}>
68
+ {day.map((block, j) => block
69
+ ? <div key={j} style={{height: block[0], borderRadius: 1, background: `var(--c-lavender-${block[1]})`}}></div>
70
+ : <div key={j} style={{height: 8}}></div>
71
+ )}
72
+ </div>
73
+ ))}
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </>
79
+ );
80
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Portaliq abstract — external portal, seen from the visitor's side.
3
+ *
4
+ * Inferred from the app role (public-facing portals on top of the
5
+ * workspace): a slim public header instead of the full Nextcloud icon
6
+ * row, then a submission form card next to a message inbox list. No
7
+ * left nav — the visitor gets one page, not an admin surface. Tones:
8
+ * cobalt for the frame, mint for the delivered/confirmed states.
9
+ */
10
+
11
+ import React from 'react';
12
+ import styles from '../AppMock.module.css';
13
+
14
+ export default function PortaliqMock() {
15
+ return (
16
+ <>
17
+ {/* Public header: logo + a few page links, no app-icon strip */}
18
+ <div className={styles.topbar}>
19
+ <div className={styles.logo}></div>
20
+ <div className={styles.spacer}></div>
21
+ {Array.from({length: 3}).map((_, i) => <div key={i} className={styles.icon}></div>)}
22
+ <div className={styles.avatar}></div>
23
+ </div>
24
+ <div className={[styles.body, styles.decidesk].filter(Boolean).join(' ')}>
25
+ <div className={styles.col}>
26
+ {/* Portal masthead */}
27
+ <div className={styles.head}>
28
+ <div className={styles.row + ' ' + styles.head} style={{width: '35%'}}></div>
29
+ <div className={styles.statusPill}>
30
+ <div className={styles.h}></div>
31
+ <div className={styles.t}></div>
32
+ </div>
33
+ </div>
34
+ <div className={styles.panelRow} style={{gridTemplateColumns: '1.2fr 1fr'}}>
35
+ {/* Submission form card */}
36
+ <div className={styles.panel}>
37
+ <div className={styles.head}><div className={styles.title}></div></div>
38
+ <div className={styles.row + ' ' + styles.head} style={{width: '25%', height: 4}}></div>
39
+ <div style={{height: 14, background: 'white', border: '1px solid var(--c-cobalt-200)', borderRadius: 3}}></div>
40
+ <div className={styles.row + ' ' + styles.head} style={{width: '30%', height: 4}}></div>
41
+ <div style={{height: 14, background: 'white', border: '1px solid var(--c-cobalt-200)', borderRadius: 3}}></div>
42
+ <div className={styles.row + ' ' + styles.head} style={{width: '20%', height: 4}}></div>
43
+ <div style={{height: 26, background: 'white', border: '1px solid var(--c-cobalt-200)', borderRadius: 3}}></div>
44
+ <div className={styles.actions} style={{justifyContent: 'flex-end', marginTop: 2}}>
45
+ <div className={styles.btn}></div>
46
+ </div>
47
+ </div>
48
+ {/* Message inbox list */}
49
+ <div className={[styles.panel, styles['w-mail']].join(' ')}>
50
+ <div className={styles.head}><div className={styles.title}></div></div>
51
+ <div className={styles.list}>
52
+ <div className={styles.item}><div className={styles.av}></div><div className={styles.lines}><div className={styles.l1}></div><div className={styles.l2}></div></div></div>
53
+ <div className={styles.item}><div className={styles.av}></div><div className={styles.lines}><div className={styles.l1}></div><div className={styles.l2}></div></div></div>
54
+ <div className={styles.item}><div className={styles.av}></div><div className={styles.lines}><div className={styles.l1}></div><div className={styles.l2}></div></div></div>
55
+ <div className={styles.item}><div className={styles.av}></div><div className={styles.lines}><div className={styles.l1}></div><div className={styles.l2}></div></div></div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </>
62
+ );
63
+ }