anentrypoint-design 0.0.414 → 0.0.416

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 (102) hide show
  1. package/dist/247420.js +62 -12
  2. package/dist/app-shell.css +40 -594
  3. package/dist/colors_and_type.css +607 -47
  4. package/dist/index.html +227 -277
  5. package/dist/preview/buttons.html +39 -28
  6. package/dist/preview/colors-core.html +18 -8
  7. package/dist/preview/colors-lore.html +26 -17
  8. package/dist/preview/colors-semantic.html +24 -18
  9. package/dist/preview/dateline.html +6 -5
  10. package/dist/preview/dropzone.html +4 -3
  11. package/dist/preview/file-grid.html +10 -9
  12. package/dist/preview/file-row.html +14 -13
  13. package/dist/preview/file-toolbar.html +4 -3
  14. package/dist/preview/file-viewer.html +4 -3
  15. package/dist/preview/header.html +26 -24
  16. package/dist/preview/icons-unicode.html +8 -5
  17. package/dist/preview/index-row.html +17 -9
  18. package/dist/preview/inputs.html +4 -3
  19. package/dist/preview/manifesto.html +34 -17
  20. package/dist/preview/motion-default.js +77 -57
  21. package/dist/preview/rules.html +4 -3
  22. package/dist/preview/spacing.html +13 -12
  23. package/dist/preview/stamps-lore.html +5 -4
  24. package/dist/preview/stamps.html +4 -3
  25. package/dist/preview/theme-ink.html +7 -6
  26. package/dist/preview/type-display.html +14 -5
  27. package/dist/preview/type-mono.html +6 -5
  28. package/dist/preview/type-prose.html +12 -10
  29. package/dist/preview/type-scale.html +4 -3
  30. package/dist/preview/wordmarks.html +10 -7
  31. package/dist/site/content/globals/site.yaml +5 -3
  32. package/dist/site/content/pages/freddie.yaml +2 -2
  33. package/dist/site/content/pages/home.yaml +55 -55
  34. package/dist/site/theme.mjs +96 -348
  35. package/dist/slides/index.html +13 -13
  36. package/dist/src/bootstrap.js +10 -1
  37. package/dist/src/components/chat.js +43 -195
  38. package/dist/src/components/community.js +18 -166
  39. package/dist/src/components/content.js +30 -210
  40. package/dist/src/components/files-modals.js +14 -106
  41. package/dist/src/components/files.js +15 -116
  42. package/dist/src/components/freddie/helpers.js +53 -8
  43. package/dist/src/components/freddie.js +35 -25
  44. package/dist/src/components/shell.js +16 -114
  45. package/dist/src/components/theme-toggle.js +69 -25
  46. package/dist/src/components.js +92 -13
  47. package/dist/src/deck-stage.js +6 -4
  48. package/dist/src/highlight.js +51 -7
  49. package/dist/src/index.js +58 -12
  50. package/dist/src/kits/os/about-app.js +2 -3
  51. package/dist/src/kits/os/app-panes.css +46 -70
  52. package/dist/src/kits/os/browser-app.js +28 -1
  53. package/dist/src/kits/os/files-app.js +7 -0
  54. package/dist/src/kits/os/freddie/pages-chat.js +90 -75
  55. package/dist/src/kits/os/freddie/pages-core.js +9 -9
  56. package/dist/src/kits/os/freddie/pages-os.js +8 -8
  57. package/dist/src/kits/os/freddie/pages-tools.js +14 -14
  58. package/dist/src/kits/os/freddie/routes.js +21 -19
  59. package/dist/src/kits/os/freddie-dashboard.css +42 -21
  60. package/dist/src/kits/os/freddie-dashboard.js +6 -6
  61. package/dist/src/kits/os/icons.js +38 -16
  62. package/dist/src/kits/os/index.js +11 -0
  63. package/dist/src/kits/os/launcher.css +26 -10
  64. package/dist/src/kits/os/launcher.js +3 -0
  65. package/dist/src/kits/os/shell.js +72 -108
  66. package/dist/src/kits/os/terminal-app.js +1 -1
  67. package/dist/src/kits/os/theme.css +1330 -150
  68. package/dist/src/kits/os/wm.css +68 -15
  69. package/dist/src/kits/os/wm.js +93 -17
  70. package/dist/src/markdown.js +121 -12
  71. package/dist/src/motion.js +18 -2
  72. package/dist/src/page-html.js +62 -134
  73. package/dist/src/theme.js +106 -13
  74. package/dist/src/web-components/ds-chat.js +80 -9
  75. package/dist/ui_kits/aicat/app.js +54 -6
  76. package/dist/ui_kits/aicat/index.html +2 -0
  77. package/dist/ui_kits/blog/index.html +33 -17
  78. package/dist/ui_kits/chat/app.js +90 -22
  79. package/dist/ui_kits/chat/index.html +2 -0
  80. package/dist/ui_kits/community/app.js +86 -24
  81. package/dist/ui_kits/community/index.html +1 -0
  82. package/dist/ui_kits/dashboard/app.js +123 -34
  83. package/dist/ui_kits/dashboard/index.html +1 -0
  84. package/dist/ui_kits/docs/index.html +43 -33
  85. package/dist/ui_kits/error_404/app.js +78 -23
  86. package/dist/ui_kits/file_browser/README.md +1 -1
  87. package/dist/ui_kits/file_browser/app.js +45 -10
  88. package/dist/ui_kits/file_browser/index.html +1 -0
  89. package/dist/ui_kits/gallery/app.js +130 -51
  90. package/dist/ui_kits/homepage/app.js +156 -138
  91. package/dist/ui_kits/project_page/app.js +142 -125
  92. package/dist/ui_kits/project_page/index.html +2 -1
  93. package/dist/ui_kits/search/app.js +52 -15
  94. package/dist/ui_kits/settings/app.js +166 -39
  95. package/dist/ui_kits/signin/app.js +175 -49
  96. package/dist/ui_kits/signin/index.html +1 -0
  97. package/dist/ui_kits/slide_deck/app.js +83 -49
  98. package/dist/ui_kits/system_primer/app.js +57 -40
  99. package/dist/ui_kits/terminal/app.js +160 -43
  100. package/package.json +1 -1
  101. package/src/page-html/client-script.js +40 -11
  102. package/src/page-html/page-styles.js +22 -1
@@ -30,25 +30,41 @@
30
30
  <link rel="stylesheet" href="../../colors_and_type.css">
31
31
  <link rel="stylesheet" href="../../app-shell.css">
32
32
  <style>
33
- .post-meta{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:24px;font-family:var(--ff-mono);font-size:14px;text-transform:uppercase;letter-spacing:0.08em;color:var(--panel-text-2);}
33
+ .post-meta{display:flex;gap:var(--space-2-5);flex-wrap:wrap;margin-bottom:var(--space-4);font-family:var(--ff-mono);font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:0.08em;color:var(--panel-text-2);}
34
34
  .post-meta .sep{color:var(--panel-text-3);}
35
- blockquote{margin:20px 0;padding:12px 18px;background:var(--panel-1);border-radius:8px;font-size:15px;color:var(--panel-text);line-height:1.55;max-width:62ch;}
36
- blockquote::before{content:'“';color:var(--panel-accent);font-family:var(--ff-display);font-size:28px;margin-right:6px;line-height:0;vertical-align:-4px;}
37
- .post-body p{font-size:15px;line-height:1.7;max-width:62ch;color:var(--panel-text);}
38
- .post-body a{color:var(--panel-accent);}
35
+ /* padding right/left is 18px: off-scale between --space-3 (16px) and
36
+ --space-3-5 (20px), deliberately. A pull-quote's side inset is set against
37
+ the 62ch measure, not the 8pt rhythm — snapping to 16px lets the quote
38
+ text sit flush enough with body prose to stop reading as a quote, and 20px
39
+ over-indents it past the accent rule. Kept as a literal on purpose. */
40
+ blockquote{margin:var(--space-3-5) 0;padding:var(--space-2-75) 18px;background:var(--panel-1);border-radius:var(--r-1);font-size:var(--fs-sm);color:var(--panel-text);line-height:1.55;max-width:62ch;}
41
+ /* font-size is the GLYPH size of the decorative open-quote mark, matched to
42
+ the mark's optical weight beside 15px prose, not a type-ladder tier — the
43
+ same treatment lint-tokens.mjs gives icon sizes set via font-size. */
44
+ blockquote::before{content:'“';color:var(--panel-accent);font-family:var(--ff-display);font-size:28px;margin-right:var(--space-1-75);line-height:0;vertical-align:-4px;}
45
+ .post-body p{font-size:var(--fs-sm);line-height:1.7;max-width:62ch;color:var(--panel-text);}
46
+ /* --panel-accent is the FILL lime (~1.07:1 on paper — invisible as text).
47
+ Prose links must use the readable TEXT tone, which is exactly what
48
+ base.css's `.ds-prose a` already sets, so this rule only needs to not
49
+ fight it. Kept explicit so the intent survives the next edit. */
50
+ .post-body a{color:var(--accent-ink);}
39
51
  </style>
40
52
 
41
53
  </head>
42
54
  <body data-screen-label="04 Blog">
43
55
  <div class="app">
56
+ <!-- Matches what AppShell() emits for the JS-rendered kits. This page hand-
57
+ rolls the shell markup statically, so it does not inherit the skip link
58
+ and had no way past the header nav for a keyboard user. -->
59
+ <a href="#app-main" class="skip-link">skip to main content</a>
44
60
  <header class="app-topbar">
45
61
  <span class="brand">247420<span class="slash"> / </span>writing</span>
46
62
  <nav>
47
- <a href="../homepage/">← homepage</a>
48
- <a href="#" class="active">lore</a>
49
- <a href="#">gm</a>
50
- <a href="#">manifesto</a>
51
- <a href="#">notes</a>
63
+ <a href="../homepage/"><- homepage</a>
64
+ <a class="active" aria-current="page">lore</a>
65
+ <a class="is-unwritten" title="no posts under this tag yet">gm</a>
66
+ <a class="is-unwritten" title="no posts under this tag yet">manifesto</a>
67
+ <a class="is-unwritten" title="no posts under this tag yet">notes</a>
52
68
  </nav>
53
69
  </header>
54
70
  <div class="app-crumb">
@@ -58,9 +74,9 @@
58
74
  <span class="leaf">we-were-here-first.md</span>
59
75
  </div>
60
76
  <div class="app-body no-side">
61
- <main class="app-main narrow centered">
77
+ <main class="app-main narrow" id="app-main" tabindex="0">
62
78
  <div class="post-meta">
63
- <spanlore</span><span class="sep">·</span>
79
+ <span>lore</span><span class="sep">·</span>
64
80
  <span>apr 14 2026</span><span class="sep">·</span>
65
81
  <span>8 min read</span><span class="sep">·</span>
66
82
  <span><span class="chip accent">draft</span></span>
@@ -71,20 +87,20 @@
71
87
  <p>there's a particular feeling — you know the one — of watching something you made five years ago get repackaged, re-skinned, and re-announced as if it were new. the web is a very long memory with a very short attention span, and 247420 has been, quietly, <em>around</em> for most of it.</p>
72
88
  <p>this isn't a victory lap. it's a map. because if you're showing up to the party now, you should at least know who set the table.</p>
73
89
  <blockquote>we don't know which ideas become the future. we just know most of them come from the same few rooms.</blockquote>
74
- <p>the rooms have moved from irc to forums to discord to whatever-this-is-now but the pattern is consistent. a handful of people, each a little too weird to hold a normal job, trading prototypes at 2am, throwing away 90% of them, shipping the 10% that's too strange to ignore. that's the collective. that's always been the collective. <a href="#">adaptogen</a>, <code>gm</code>, zellous, <a href="#">the long list of things you've never heard of</a> all came out of the same 2am.</p>
90
+ <p>the rooms have moved, from irc to forums to discord to whatever-this-is-now, but the pattern is consistent. a handful of people, each a little too weird to hold a normal job, trading prototypes at 2am, throwing away 90% of them, shipping the 10% that's too strange to ignore. that's the collective. that's always been the collective. <a href="https://github.com/AnEntrypoint/mutagen">adaptogen</a>, <code>gm</code>, zellous, and <a href="https://github.com/AnEntrypoint">the long list of things you've never heard of</a> all came out of the same 2am.</p>
75
91
  <h2>so what's the actual point</h2>
76
92
  <p>the point is: stop asking permission. ship the rough draft. document honestly. humor is load-bearing. and if you're reading this thinking "that's obvious" — good. you're home.</p>
77
- <p class="ds-prose-endnote"end. reply by opening a pr. <a href="#">source ↗</a></p>
93
+ <p class="ds-prose-endnote">end. reply by opening a pr. <a href="https://github.com/AnEntrypoint/design">source -></a></p>
78
94
  </div>
79
95
  </main>
80
96
  </div>
81
97
  <footer class="app-status">
82
98
  <span class="item">main</span>
83
- <span class="item">• markdown</span>
84
- <span class="item">• 8 min</span>
99
+ <span class="item">- markdown</span>
100
+ <span class="item">- 8 min</span>
85
101
  <span class="spread"></span>
86
102
  <span class="item">apr 14 2026</span>
87
- <span class="item">• draft</span>
103
+ <span class="item">- draft</span>
88
104
  </footer>
89
105
  </div>
90
106
  </body></html>
@@ -7,7 +7,7 @@ const h = webjsx.createElement;
7
7
  const seed = [
8
8
  { who: 'them', avatar: 'jr', name: 'jordan', time: '14:02',
9
9
  parts: [{ kind: 'text', text: 'pushed v0.0.27, theme cleanup looks clean now. see the **release notes** in [the changelog](https://github.com/AnEntrypoint/design/releases).' }],
10
- reactions: [{ emoji: '🎉', count: 3, you: true }, { emoji: '👀', count: 1 }] },
10
+ reactions: [{ emoji: 'yay', count: 3, you: true }, { emoji: 'eyes', count: 1 }] },
11
11
  { who: 'them', avatar: 'mk', name: 'mai', time: '14:03',
12
12
  parts: [{ kind: 'text', text: 'nice. body-hide trick on first paint? share the diff?' }] },
13
13
  { who: 'you', avatar: 'me', time: '14:04', receipt: 'read',
@@ -18,9 +18,9 @@ const seed = [
18
18
  ] },
19
19
  { who: 'them', avatar: 'jr', name: 'jordan', time: '14:05',
20
20
  parts: [{ kind: 'md', text: '## review notes\n\nlooks solid. couple things:\n\n- short timeout fallback in case fonts hang\n- announce the `ready` class via `requestIdleCallback`\n- keep no-js fallback to `visibility: visible`\n\n> "ship the rough draft" — but not the broken one.\n\nwill review the rest tonight.' }],
21
- reactions: [{ emoji: '', count: 2, you: true }] },
21
+ reactions: [{ emoji: 'done', count: 2, you: true }] },
22
22
  { who: 'them', avatar: 'mk', name: 'mai', time: '14:08',
23
- parts: [{ kind: 'image', src: './sample-svg.svg', alt: 'design system mascot', caption: 'spot the new mascot — final' }] },
23
+ parts: [{ kind: 'image', src: './sample-svg.svg', alt: 'design system mascot', caption: 'spot the new mascot — final' }] },
24
24
  { who: 'you', avatar: 'me', time: '14:10', receipt: 'read',
25
25
  parts: [
26
26
  { kind: 'text', text: 'attaching the v0.0.27 token sheet for review:' },
@@ -33,10 +33,41 @@ const seed = [
33
33
  thumb: './sample-square.png' }] },
34
34
  { who: 'them', avatar: 'mk', name: 'mai', time: '14:14',
35
35
  parts: [{ kind: 'file', src: './sample.pdf', name: 'meeting-notes-2026-05-01.pdf', size: 782 }],
36
- reactions: [{ emoji: '📌', count: 1 }] }
36
+ reactions: [{ emoji: 'pin', count: 1 }] },
37
+ { who: 'them', avatar: 'jr', name: 'jordan', time: '14:15', typing: true,
38
+ parts: [] }
37
39
  ];
38
40
 
39
- const state = { draft: '', room: 'general', messages: seed.slice() };
41
+ // `phase` drives the thread. Chat() already owns the empty state (its
42
+ // .chat-empty block), so `empty` here just hands it zero messages plus the
43
+ // room-specific copy; loading and error are rendered by this kit around it.
44
+ const state = { draft: '', room: 'general', messages: seed.slice(), phase: 'ready' };
45
+ const PHASES = ['ready', 'loading', 'empty', 'error'];
46
+
47
+ // Message-shaped shimmer. Reuses .ds-event-row-skeleton + .ds-skel* from
48
+ // app-shell/files.css — the avatar/body/timestamp rhythm is the same shape.
49
+ function ThreadSkeleton() {
50
+ return Panel({ title: 'loading #' + state.room, children: h('div', {},
51
+ ...Array.from({ length: 6 }, (_, i) => h('div', { key: 'sk' + i, class: 'ds-event-row-skeleton' },
52
+ h('span', { class: 'ds-skel ds-skel-icon' }),
53
+ h('span', { class: 'ds-skel ds-skel-title' }),
54
+ h('span', { class: 'ds-skel ds-skel-meta' })
55
+ ))
56
+ ) });
57
+ }
58
+
59
+ function ThreadError() {
60
+ return Panel({ title: 'thread unavailable', children: h('div', { class: 'ds-alert ds-alert-error' },
61
+ h('span', { class: 'ds-alert-icon' }, '!'),
62
+ h('div', { class: 'ds-alert-content' },
63
+ h('div', { class: 'ds-alert-title' }, 'lost the socket to #' + state.room),
64
+ h('div', { class: 'ds-alert-message' }, 'the connection dropped mid-sync, so the last few messages may be missing and anything you send now would not leave this tab. reconnecting replays from the last message you saw.'),
65
+ h('div', { class: 'ds-alert-retry' },
66
+ h('button', { class: 'btn', onclick: () => { state.phase = 'ready'; kit.render(); } }, 'reconnect')
67
+ )
68
+ )
69
+ ) });
70
+ }
40
71
  const rooms = [
41
72
  { glyph: '#', label: 'general', count: 12, key: 'general' },
42
73
  { glyph: '#', label: 'design', count: 4, key: 'design' },
@@ -71,38 +102,75 @@ function send(text) {
71
102
 
72
103
  function App() {
73
104
  return AppShell({
74
- topbar: Topbar({ brand: '247420', leaf: 'chat', items: [['index', '../../'], ['aicat', '../aicat/'], ['source ', 'https://github.com/AnEntrypoint/design']] }),
105
+ topbar: Topbar({ brand: '247420', leaf: 'chat', items: [['index', '../../'], ['aicat', '../aicat/'], ['source ->', 'https://github.com/AnEntrypoint/design']] }),
75
106
  crumb: Crumb({ trail: ['247420', 'kits'], leaf: 'chat' }),
76
107
  side: Side({
77
108
  sections: [
78
109
  { group: 'rooms', items: rooms.map(r => ({ ...r, active: state.room === r.key, onClick: (e) => { e.preventDefault(); state.room = r.key; kit.render(); } })) },
79
- { group: 'direct', items: dms.map(r => ({ ...r, active: state.room === r.key, onClick: (e) => { e.preventDefault(); state.room = r.key; kit.render(); } })) }
110
+ { group: 'direct', items: dms.map(r => ({ ...r, active: state.room === r.key, onClick: (e) => { e.preventDefault(); state.room = r.key; kit.render(); } })) },
111
+ // Reachable state switcher for the thread.
112
+ { group: 'thread state', items: PHASES.map((p) => ({
113
+ glyph: h('span', { class: state.phase === p ? 'ds-dot ds-dot-on' : 'ds-dot ds-dot-off' }),
114
+ label: p, key: 'ph-' + p, active: state.phase === p,
115
+ onClick: (e) => { e.preventDefault(); state.phase = p; kit.render(); }
116
+ })) }
80
117
  ]
81
118
  }),
82
119
  main: [
83
- h('div', { class: 'ds-section' },
84
- h('h1', {}, '# ' + state.room),
85
- h('p', { class: 'lede' }, 'thread of messages with rich attachments — text, code (prism-highlighted), image, pdf, file, link, markdown (marked + DOMPurify), reactions, read-receipts.'),
86
- Chat({
87
- title: state.room, sub: 'public', messages: state.messages,
88
- composer: ChatComposer({
89
- value: state.draft,
90
- placeholder: 'message #' + state.room + '',
91
- onInput: (v) => { state.draft = v; kit.render(); },
92
- onSend: send
93
- })
94
- }),
120
+ // Every other AppShell kit names its page with an h1; this one had
121
+ // none, so the document went straight from <main> to the thread and
122
+ // a screen reader's heading list came back empty. Visually hidden
123
+ // rather than drawn, because Chat() already renders the room name as
124
+ // its own visible title a second visible copy would be redundant.
125
+ h('h1', { class: 'sr-only' }, 'chat — #' + state.room),
126
+ h('div', { class: 'ds-section chat-kit-page' },
127
+ h('div', { class: 'ds-chat-layout' },
128
+ state.phase === 'loading' ? ThreadSkeleton()
129
+ : state.phase === 'error' ? ThreadError()
130
+ : Chat({
131
+ title: state.room,
132
+ // Chat()'s own empty block uses `sub` as its body line,
133
+ // so the empty phase gets copy that names what belongs
134
+ // here and what puts it here — not a bare "no messages".
135
+ sub: state.phase === 'empty'
136
+ ? 'nobody has posted in #' + state.room + ' yet. say something and it becomes the first message everyone sees on join.'
137
+ : 'public',
138
+ messages: state.phase === 'empty' ? [] : state.messages,
139
+ composer: ChatComposer({
140
+ value: state.draft,
141
+ placeholder: 'message #' + state.room + '…',
142
+ onInput: (v) => { state.draft = v; kit.render(); },
143
+ onSend: send
144
+ })
145
+ }),
146
+ // Persistent detail rail — only revealed once .ds-chat-layout has
147
+ // room to spare (>=1100px, see app-shell.css). On mobile/tablet
148
+ // this is display:none rather than reflowed below the thread, so
149
+ // the composer stays the last on-screen element there.
150
+ h('div', { class: 'ds-chat-detail' },
151
+ Panel({ title: 'this room', children: h('div', { class: 'ds-pattern-notes' },
152
+ h('p', {}, h('strong', {}, '#' + state.room)),
153
+ h('p', {}, rooms.find(r => r.key === state.room)?.count ?? dms.find(r => r.key === state.room)?.count ?? 0, ' members')
154
+ ) }),
155
+ Panel({ title: 'participants', children:
156
+ [{ glyph: '·', label: 'jordan' }, { glyph: '·', label: 'mai' }].map((p, i) =>
157
+ h('div', { key: 'p' + i, class: 'ds-pattern-notes' }, h('p', {}, p.glyph + ' ' + p.label))
158
+ )
159
+ })
160
+ )
161
+ ),
95
162
  Panel({
96
163
  title: 'pattern notes',
97
164
  children: h('div', { class: 'ds-pattern-notes' },
98
165
  h('p', {}, '· bubble corner-cut on the originating side (4–6px) gives directional read without arrows.'),
99
- h('p', {}, '· own messages take the accent fill so the eye lands on what you said last; delivered, ✓✓ read.'),
100
- h('p', {}, '· markdown is parsed by ', h('code', {}, 'marked'), ' and sanitized by ', h('code', {}, 'DOMPurify'), '; code blocks lit by ', h('code', {}, 'prism.js'), '.')
166
+ h('p', {}, '· own messages take the accent fill so the eye lands on what you said last; [x] delivered, [x][x] read.'),
167
+ h('p', {}, '· markdown is parsed by ', h('code', {}, 'marked'), ' and sanitized by ', h('code', {}, 'DOMPurify'), '; code blocks lit by ', h('code', {}, 'prism.js'), '.'),
168
+ h('p', {}, '· >=1100px viewport reveals a persistent "this room" + participants rail beside the thread instead of just widening the message column.')
101
169
  )
102
170
  })
103
171
  )
104
172
  ],
105
- status: Status({ left: ['main', ' ' + state.messages.length + ' messages', ' ' + rooms.length + ' rooms'], right: ['247420 / mmxxvi'] })
173
+ status: Status({ left: ['main', '- ' + (state.phase === 'ready' ? state.messages.length : 0) + ' messages', '- ' + rooms.length + ' rooms', '- ' + state.phase], right: ['247420 / mmxxvi'] })
106
174
  });
107
175
  }
108
176
 
@@ -11,6 +11,8 @@
11
11
  <link rel="icon" type="image/svg+xml" href="../../favicon.svg">
12
12
  <link rel="stylesheet" href="../../colors_and_type.css">
13
13
  <link rel="stylesheet" href="../../app-shell.css">
14
+ <link rel="stylesheet" href="../../chat.css">
15
+ <link rel="stylesheet" href="../../editor-primitives.css">
14
16
  <script type="importmap">
15
17
  { "imports": {
16
18
  "webjsx": "../../vendor/webjsx/index.js",
@@ -32,9 +32,9 @@ const channels = [
32
32
 
33
33
  const members = [
34
34
  { label: 'online — 3', members: [
35
- { identity: 'jordan', name: 'jordan', status: 'online', color: '#3F8A4A' },
36
- { identity: 'mai', name: 'mai', status: 'online', color: '#6B3A78' },
37
- { identity: 'aicat', name: 'aicat', status: 'online', color: '#F07AA8' },
35
+ { identity: 'jordan', name: 'jordan', status: 'online', color: 'var(--cat-green)' },
36
+ { identity: 'mai', name: 'mai', status: 'online', color: 'var(--cat-purple)' },
37
+ { identity: 'aicat', name: 'aicat', status: 'online', color: 'var(--cat-mascot)' },
38
38
  ]},
39
39
  { label: 'offline — 2', members: [
40
40
  { identity: 'river', name: 'river', status: 'offline' },
@@ -47,44 +47,101 @@ const state = {
47
47
  activeChannel: channels[2],
48
48
  collapsedCats: new Set(),
49
49
  memberListOpen: false,
50
+ // Which state the message list renders in; cycled from the channel header.
51
+ phase: 'ready',
50
52
  voiceOpen: false,
51
53
  muted: false,
52
54
  deafened: false,
53
55
  messages: [
54
- { id: 1, author: 'jordan', color: '#3F8A4A', time: '14:02', text: 'shipped the community shell component. check it out.' },
55
- { id: 2, author: 'mai', color: '#6B3A78', time: '14:03', text: 'looks clean. does it handle collapsed cats?' },
56
- { id: 3, author: 'aicat', color: '#F07AA8', time: '14:04', text: 'yes click any category header.' },
56
+ // `color` is the FILL (avatar disc, ink text on top); `ink` is the
57
+ // readable TEXT tone for the author name. Same --accent/--accent-ink
58
+ // split the lead accent draws the bare hue as `color:` on paper is
59
+ // ~3.1:1 and fails AA.
60
+ { id: 1, author: 'jordan', color: 'var(--cat-green)', ink: 'var(--cat-green-ink)', time: '14:02', text: 'shipped the community shell component. check it out.' },
61
+ { id: 2, author: 'mai', color: 'var(--cat-purple)', ink: 'var(--cat-purple-ink)', time: '14:03', text: 'looks clean. does it handle collapsed cats?' },
62
+ { id: 3, author: 'aicat', color: 'var(--cat-mascot)', ink: 'var(--cat-mascot-ink)', time: '14:04', text: 'yes — click any category header.' },
57
63
  ]
58
64
  };
59
65
 
60
66
  const root = document.getElementById('root');
61
67
 
68
+ const PHASES = ['ready', 'loading', 'empty', 'error'];
69
+
70
+ // Message-shaped shimmer. Reuses .ds-event-row-skeleton + .ds-skel*
71
+ // (app-shell/files.css) — avatar / body / timestamp is the same rhythm.
72
+ function MessagesSkeleton() {
73
+ return h('div', { class: 'ds-community-messages' },
74
+ ...Array.from({ length: 6 }, (_, i) => h('div', { key: 'sk' + i, class: 'ds-event-row-skeleton' },
75
+ h('span', { class: 'ds-skel ds-skel-icon' }),
76
+ h('span', { class: 'ds-skel ds-skel-title' }),
77
+ h('span', { class: 'ds-skel ds-skel-meta' })
78
+ ))
79
+ );
80
+ }
81
+
82
+ function MessagesEmpty(name) {
83
+ return h('div', { class: 'ds-community-messages' },
84
+ h('div', { class: 'ds-empty-state' },
85
+ h('div', { class: 'ds-empty-state-glyph' }, '[ ]'),
86
+ h('p', { class: 'ds-empty-state-msg' }, 'nothing posted in #' + name + ' yet'),
87
+ h('p', { class: 'ds-empty-state-hint' }, 'this is the start of the channel. the first message here is what everyone sees when they join it.')
88
+ )
89
+ );
90
+ }
91
+
92
+ function MessagesError(name, onRetry) {
93
+ return h('div', { class: 'ds-community-messages' },
94
+ h('div', { class: 'ds-alert ds-alert-error' },
95
+ h('span', { class: 'ds-alert-icon' }, '!'),
96
+ h('div', { class: 'ds-alert-content' },
97
+ h('div', { class: 'ds-alert-title' }, 'cannot load #' + name),
98
+ h('div', { class: 'ds-alert-message' }, 'the gateway accepted the join but never sent history, so this channel is connected and blank rather than actually empty. rejoining requests the backlog again.'),
99
+ h('div', { class: 'ds-alert-retry' },
100
+ h('button', { class: 'btn', onclick: onRetry }, 'rejoin channel')
101
+ )
102
+ )
103
+ )
104
+ );
105
+ }
106
+
107
+ function MessagesBody(name) {
108
+ if (state.phase === 'loading') return MessagesSkeleton();
109
+ if (state.phase === 'error') return MessagesError(name, () => { state.phase = 'ready'; kit.render(); });
110
+ if (state.phase === 'empty') return MessagesEmpty(name);
111
+ return h('div', { class: 'ds-community-messages' },
112
+ ...state.messages.map(m => h('div', { class: 'ds-community-msg', key: String(m.id) },
113
+ h('div', { class: 'ds-community-avatar', style: `background:${m.color || 'var(--panel-3)'}` }, m.author[0].toUpperCase()),
114
+ h('div', { class: 'ds-community-msg-body' },
115
+ h('span', { class: 'ds-community-msg-name', style: `color:${m.ink || m.color || 'var(--fg)'}` }, m.author),
116
+ h('span', { class: 'ds-community-msg-time' }, m.time),
117
+ h('p', { class: 'ds-community-msg-text' }, m.text)
118
+ )
119
+ ))
120
+ );
121
+ }
122
+
62
123
  function App() {
63
124
  const ch = state.activeChannel;
64
125
  const chatContent = h('div', { class: 'ds-community-main' },
65
126
  ChatHeader({
66
- icon: ch.type === 'voice' ? '🔊' : '#',
127
+ icon: ch.type === 'voice' ? '((' : '#',
67
128
  name: ch.name,
68
129
  topic: ch.type === 'text' ? 'community shell demo — click channels to switch' : null,
69
130
  toolbar: [
70
- h('button', { class: 'btn btn-ghost', onclick: () => { state.memberListOpen = !state.memberListOpen; kit.render(); } },
71
- state.memberListOpen ? '✕ members' : ' members')
131
+ // Reachable state switcher for the message list.
132
+ h('button', { key: 'phase', class: 'btn btn-ghost', onclick: () => {
133
+ state.phase = PHASES[(PHASES.indexOf(state.phase) + 1) % PHASES.length];
134
+ kit.render();
135
+ } }, 'state: ' + state.phase),
136
+ h('button', { key: 'members', class: 'btn btn-ghost', onclick: () => { state.memberListOpen = !state.memberListOpen; kit.render(); } },
137
+ state.memberListOpen ? 'hide members' : 'show members')
72
138
  ]
73
139
  }),
74
- h('div', { class: 'ds-community-messages' },
75
- ...state.messages.map(m => h('div', { class: 'ds-community-msg', key: String(m.id) },
76
- h('div', { class: 'ds-community-avatar', style: `background:${m.color || 'var(--panel-3)'}` }, m.author[0].toUpperCase()),
77
- h('div', { class: 'ds-community-msg-body' },
78
- h('span', { class: 'ds-community-msg-name', style: `color:${m.color || 'var(--fg)'}` }, m.author),
79
- h('span', { class: 'ds-community-msg-time' }, m.time),
80
- h('p', { class: 'ds-community-msg-text' }, m.text)
81
- )
82
- ))
83
- )
140
+ MessagesBody(ch.name)
84
141
  );
85
142
 
86
143
  return h('div', { class: 'ds-community-page' },
87
- Topbar({ brand: '247420', leaf: 'community', items: [['index', '../../'], ['chat', '../chat/'], ['source ', 'https://github.com/AnEntrypoint/design']] }),
144
+ Topbar({ brand: '247420', leaf: 'community', items: [['index', '../../'], ['chat', '../chat/'], ['source ->', 'https://github.com/AnEntrypoint/design']] }),
88
145
  CommunityShell({
89
146
  serverRailProps: {
90
147
  servers,
@@ -104,13 +161,18 @@ function App() {
104
161
  },
105
162
  userPanelProps: {
106
163
  name: 'you',
107
- tag: '',
108
- color: '#247420',
164
+ tag: '@',
165
+ color: 'var(--accent)',
109
166
  muted: state.muted,
110
167
  deafened: state.deafened,
111
168
  onMute: () => { state.muted = !state.muted; kit.render(); },
112
169
  onDeafen: () => { state.deafened = !state.deafened; kit.render(); },
113
- onSettings: () => {}
170
+ // 'Audio settings' opens the voice strip, which is this
171
+ // kit's actual audio surface (mute/deafen/leave). UserPanel
172
+ // renders this button whether or not a handler is passed,
173
+ // so leaving it unwired was a permanently inert control
174
+ // rather than an absent one.
175
+ onSettings: () => { state.voiceOpen = true; kit.render(); }
114
176
  }
115
177
  },
116
178
  children: chatContent,
@@ -126,7 +188,7 @@ function App() {
126
188
  open: true
127
189
  } : null
128
190
  }),
129
- Status({ left: ['community', ' ' + channels.length + ' channels', ' ' + servers.length + ' servers'], right: ['247420 / mmxxvi'] })
191
+ Status({ left: ['community', '- ' + channels.length + ' channels', '- ' + servers.length + ' servers', '- ' + state.phase], right: ['247420 / mmxxvi'] })
130
192
  );
131
193
  }
132
194
 
@@ -9,6 +9,7 @@
9
9
  <link rel="icon" type="image/svg+xml" href="../../favicon.svg">
10
10
  <link rel="stylesheet" href="../../colors_and_type.css">
11
11
  <link rel="stylesheet" href="../../app-shell.css">
12
+ <link rel="stylesheet" href="../../community.css">
12
13
  <script type="importmap">
13
14
  { "imports": {
14
15
  "webjsx": "../../vendor/webjsx/index.js",