anentrypoint-design 0.0.413 → 0.0.415

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 (103) hide show
  1. package/dist/247420.js +63 -13
  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
  103. package/src/page-html.js +10 -0
@@ -14,44 +14,103 @@ const swatchTokens = [
14
14
  { name: 'panel-select',hint: 'mint hover/select tone' }
15
15
  ];
16
16
 
17
+ // Specimens, not captions. Each tile shows an actual piece of ascii art at
18
+ // display size — the thing a gallery is for. The previous set was twelve
19
+ // equal cards whose captions were design-system documentation ("panel-on-panel
20
+ // rhythm", "1px panel-2 hairline"), so every tile read at one weight and the
21
+ // grid was a lecture wearing a gallery's layout. One of them ("no svgs in
22
+ // chrome") had also gone stale — chrome icons are Icon() line SVGs now.
23
+ //
24
+ // One tone across the whole set. The tiles previously alternated panel-1 /
25
+ // panel-2 / panel-3, which produced a checkerboard the reader has to try to
26
+ // decode — the tone changed but meant nothing, and because the grid reflows
27
+ // to the container (5-up here, 3-up narrower) any index-based tone pattern
28
+ // lands differently at every width. With the frame constant the specimen is
29
+ // the only thing that varies, which is what a gallery is for.
17
30
  const items = [
18
- { id: 'a', label: 'mascot · cat', caption: '/\\_/\\\n( o.o )', tone: 'panel-1', glyph: '' },
19
- { id: 'b', label: 'panel · stack', caption: 'panel-on-panel rhythm', tone: 'panel-2', glyph: '' },
20
- { id: 'c', label: 'rail · indicator', caption: 'color-coded inset', tone: 'panel-1', glyph: '' },
21
- { id: 'd', label: 'mono · label', caption: 'all caps · letter-spaced', tone: 'panel-2', glyph: '§' },
22
- { id: 'e', label: 'cli · prompt', caption: '$ ship it', tone: 'panel-3', glyph: '' },
23
- { id: 'f', label: 'pill · radius 999', caption: 'sidebar fab tone', tone: 'panel-1', glyph: '' },
24
- { id: 'g', label: 'badge · chip', caption: 'meta pill, dim/accent', tone: 'panel-2', glyph: '' },
25
- { id: 'h', label: 'glyph · unicode', caption: 'no svgs in chrome', tone: 'panel-1', glyph: '' },
26
- { id: 'i', label: 'manifesto · prose', caption: 'long-form, max 64ch', tone: 'panel-2', glyph: '' },
27
- { id: 'j', label: 'fade · in', caption: 'visibility-driven only', tone: 'panel-3', glyph: '' },
28
- { id: 'k', label: 'rule · divider', caption: '1px panel-2 hairline', tone: 'panel-1', glyph: '' },
29
- { id: 'l', label: 'stamp · seal', caption: 'editorial mark', tone: 'panel-2', glyph: '' }
31
+ { id: 'a', label: 'the mascot', caption: '/\\_/\\\n( o.o )\n > ^ <', tone: 'panel-2', glyph: '(=)' },
32
+ { id: 'b', label: 'the prompt', caption: '$ _', tone: 'panel-2', glyph: '$' },
33
+ { id: 'c', label: 'the seal', caption: '(( 247 ))\n(( 420 ))', tone: 'panel-2', glyph: 'O' },
34
+ { id: 'd', label: 'the arrow', caption: '-->', tone: 'panel-2', glyph: '->' },
35
+ { id: 'e', label: 'the rule', caption: '---------', tone: 'panel-2', glyph: '-' },
36
+ { id: 'f', label: 'the corner', caption: '+------\n|\n|', tone: 'panel-2', glyph: '[#]' },
37
+ { id: 'g', label: 'the stack', caption: '[###]\n [##]\n [#]', tone: 'panel-2', glyph: '[]' },
38
+ { id: 'h', label: 'the wave', caption: '~~~~~~~', tone: 'panel-2', glyph: '~' },
39
+ { id: 'i', label: 'the target', caption: '(o)', tone: 'panel-2', glyph: '(o)' },
40
+ { id: 'j', label: 'the ladder', caption: '|- - -|\n|- - -|', tone: 'panel-2', glyph: '=' },
41
+ { id: 'k', label: 'the spark', caption: '*', tone: 'panel-2', glyph: '*' },
42
+ { id: 'l', label: 'the terminus', caption: '[x]', tone: 'panel-2', glyph: '[x]' }
30
43
  ];
31
44
 
32
- const state = { open: null, density: 'comfy' };
45
+ // `phase` drives the tiles panel. A gallery is the surface where a blank grid
46
+ // is most ambiguous (still loading? nothing uploaded? request failed?), so all
47
+ // three readings get distinct, reachable copy instead of one blank box.
48
+ const state = { open: null, density: 'comfy', phase: 'ready' };
49
+ const PHASES = ['ready', 'loading', 'empty', 'error'];
50
+
51
+ // Tile-shaped shimmer. Reuses the .ds-skeleton primitive (app-shell/
52
+ // loading-alerts.css) inside the existing .ds-tile-grid so the placeholders
53
+ // occupy exactly the tracks the real tiles will.
54
+ function TilesSkeleton() {
55
+ return h('div', { class: 'ds-tile-grid' + (state.density === 'tight' ? ' ds-tile-grid--tight' : '') },
56
+ ...Array.from({ length: 8 }, (_, i) => h('div', { key: 'sk' + i, class: 'ds-gallery-tile' },
57
+ h('div', { class: 'ds-skeleton ds-skel-title' }),
58
+ h('div', { class: 'ds-skeleton ds-skel-meta' })
59
+ ))
60
+ );
61
+ }
62
+
63
+ function TilesEmpty() {
64
+ return h('div', { class: 'ds-empty-state' },
65
+ h('div', { class: 'ds-empty-state-glyph' }, '[ ]'),
66
+ h('p', { class: 'ds-empty-state-msg' }, 'no tiles in this set'),
67
+ h('p', { class: 'ds-empty-state-hint' }, 'a tile is one tonal card plus a caption. add entries to the items array and they land in this grid at whichever density is selected.')
68
+ );
69
+ }
70
+
71
+ function TilesError() {
72
+ return h('div', { class: 'ds-alert ds-alert-error' },
73
+ h('span', { class: 'ds-alert-icon' }, '!'),
74
+ h('div', { class: 'ds-alert-content' },
75
+ h('div', { class: 'ds-alert-title' }, 'tile set failed to decode'),
76
+ h('div', { class: 'ds-alert-message' }, 'four of the twelve captions came back as malformed utf-8, so the whole set was rejected rather than rendered with holes in it. re-export the set as utf-8 and reload.'),
77
+ h('div', { class: 'ds-alert-retry' },
78
+ h('button', { class: 'btn', onclick: () => { state.phase = 'ready'; kit.render(); } }, 'reload set')
79
+ )
80
+ )
81
+ );
82
+ }
83
+
84
+ function TilesBody() {
85
+ if (state.phase === 'loading') return TilesSkeleton();
86
+ if (state.phase === 'error') return TilesError();
87
+ if (state.phase === 'empty') return TilesEmpty();
88
+ return h('div', { class: 'ds-tile-grid' + (state.density === 'tight' ? ' ds-tile-grid--tight' : '') }, ...items.map(Tile));
89
+ }
33
90
 
34
91
  function Tile(it) {
35
- const size = state.density === 'tight' ? '120px' : '160px';
36
92
  return h('button', {
37
93
  key: it.id,
38
94
  onclick: () => { state.open = it.id; kit.render(); },
39
- style: 'all:unset;cursor:pointer;display:flex;flex-direction:column;gap:6px;background:var(--' + it.tone + ');padding:12px;border-radius:10px;min-height:' + size
95
+ class: 'ds-gallery-tile' + (state.density === 'tight' ? ' ds-gallery-tile--tight' : ''),
96
+ // custom-property-only inline: carries the per-tile tone, no layout
97
+ style: '--tile-tone:var(--' + it.tone + ')'
40
98
  },
41
- h('div', { style: 'flex:1;display:flex;align-items:center;justify-content:center;font-family:var(--ff-mono);white-space:pre-line;color:var(--panel-text-2);font-size:18px' }, it.caption),
42
- h('div', { style: 'display:flex;align-items:center;justify-content:space-between;gap:6px;font-size:12px' },
43
- h('span', { style: 'font-family:var(--ff-mono);color:var(--panel-text-3)' }, it.glyph),
44
- h('span', { style: 'color:var(--panel-text)' }, it.label)
99
+ h('div', { class: 'ds-tile-cap' }, it.caption),
100
+ h('div', { class: 'ds-tile-meta' },
101
+ h('span', { class: 'ds-tile-glyph' }, it.glyph),
102
+ h('span', { class: 'ds-tile-label' }, it.label)
45
103
  )
46
104
  );
47
105
  }
48
106
 
49
107
  function Swatch(t) {
50
- return h('div', { key: t.name, style: 'display:flex;flex-direction:column;gap:6px' },
51
- h('div', { style: 'height:64px;border-radius:8px;background:var(--' + t.name + ')' }),
52
- h('div', { style: 'display:flex;justify-content:space-between;font-family:var(--ff-mono);font-size:11px' },
53
- h('span', { style: 'color:var(--panel-text)' }, t.name),
54
- h('span', { style: 'color:var(--panel-text-3)' }, t.hint)
108
+ return h('div', { key: t.name, class: 'ds-swatch-col' },
109
+ // custom-property-only inline: carries the per-swatch tone, no layout
110
+ h('div', { class: 'ds-gal-swatch', style: '--swatch:var(--' + t.name + ')' }),
111
+ h('div', { class: 'ds-gal-swatch-meta' },
112
+ h('span', { class: 'ds-gal-swatch-name' }, t.name),
113
+ h('span', { class: 'ds-gal-swatch-hint' }, t.hint)
55
114
  )
56
115
  );
57
116
  }
@@ -59,34 +118,44 @@ function Swatch(t) {
59
118
  function Lightbox() {
60
119
  if (!state.open) return null;
61
120
  const it = items.find((i) => i.id === state.open);
121
+ const close = () => { state.open = null; kit.render(); };
62
122
  return h('div', {
63
- onclick: () => { state.open = null; kit.render(); },
64
- style: 'position:fixed;inset:0;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;padding:32px;z-index:50'
123
+ onclick: close,
124
+ onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); } },
125
+ tabindex: '-1',
126
+ ref: (el) => { if (el && !el._dsLbFocused) { el._dsLbFocused = true; el.focus(); } },
127
+ class: 'ds-lightbox'
65
128
  },
66
- h('div', { onclick: (e) => e.stopPropagation(),
67
- style: 'background:var(--panel-0);border-radius:14px;padding:28px;min-width:320px;max-width:520px;display:flex;flex-direction:column;gap:14px' },
68
- h('div', { style: 'display:flex;justify-content:space-between;align-items:center' },
69
- h('span', { style: 'font-family:var(--ff-mono);font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:var(--panel-text-3)' }, 'tile · ' + it.id),
70
- h('button', { class: 'btn', onclick: () => { state.open = null; kit.render(); } }, 'close')
129
+ h('div', { onclick: (e) => e.stopPropagation(), class: 'ds-lightbox-card' },
130
+ h('div', { class: 'ds-lightbox-head' },
131
+ h('span', { class: 'ds-lightbox-tag' }, 'tile · ' + it.id),
132
+ h('button', { class: 'btn', onclick: close }, 'close')
71
133
  ),
72
- h('div', { style: 'background:var(--' + it.tone + ');padding:36px;border-radius:10px;text-align:center;font-family:var(--ff-mono);white-space:pre-line;font-size:24px' }, it.caption),
73
- h('p', { style: 'margin:0' }, h('strong', {}, it.label)),
74
- h('p', { style: 'margin:0;color:var(--panel-text-2)' }, 'this lightbox uses the same tonal panel — no extra components, no shadow. backdrop is fixed inset, click outside dismisses.')
134
+ h('div', { class: 'ds-lightbox-preview', style: '--tile-tone:var(--' + it.tone + ')' }, it.caption),
135
+ h('p', { class: 'ds-m0' }, h('strong', {}, it.label)),
136
+ // Tells the reader how to leave, which is the one thing they need
137
+ // from a lightbox. It previously described its own implementation.
138
+ h('p', { class: 'ds-m0 ds-text-2' }, 'esc, or click anywhere outside, to close.')
75
139
  )
76
140
  );
77
141
  }
78
142
 
79
143
  function App() {
80
- const cols = state.density === 'tight' ? 'repeat(auto-fill, minmax(140px, 1fr))' : 'repeat(auto-fill, minmax(180px, 1fr))';
81
144
  return AppShell({
82
- topbar: Topbar({ brand: '247420', leaf: 'gallery', items: [['index', '../../'], ['source ', 'https://github.com/AnEntrypoint/design']] }),
83
- crumb: Crumb({ trail: ['247420', 'kits'], leaf: 'gallery', right: items.length + ' tiles' }),
145
+ topbar: Topbar({ brand: '247420', leaf: 'gallery', items: [['index', '../../'], ['source ->', 'https://github.com/AnEntrypoint/design']] }),
146
+ crumb: Crumb({ trail: ['247420', 'kits'], leaf: 'gallery', right: state.phase === 'ready' ? items.length + ' tiles' : state.phase }),
84
147
  side: Side({
85
148
  sections: [
86
149
  { group: 'density', items: [
87
- { glyph: state.density === 'comfy' ? '' : '', label: 'comfy', key: 'd1', onClick: (e) => { e.preventDefault(); state.density = 'comfy'; kit.render(); } },
88
- { glyph: state.density === 'tight' ? '' : '', label: 'tight', key: 'd2', onClick: (e) => { e.preventDefault(); state.density = 'tight'; kit.render(); } }
150
+ { glyph: h('span', { class: state.density === 'comfy' ? 'ds-dot ds-dot-on' : 'ds-dot ds-dot-off' }), label: 'comfy', key: 'd1', onClick: (e) => { e.preventDefault(); state.density = 'comfy'; kit.render(); } },
151
+ { glyph: h('span', { class: state.density === 'tight' ? 'ds-dot ds-dot-on' : 'ds-dot ds-dot-off' }), label: 'tight', key: 'd2', onClick: (e) => { e.preventDefault(); state.density = 'tight'; kit.render(); } }
89
152
  ] },
153
+ // Reachable state switcher for the tiles panel.
154
+ { group: 'tile state', items: PHASES.map((p) => ({
155
+ glyph: h('span', { class: state.phase === p ? 'ds-dot ds-dot-on' : 'ds-dot ds-dot-off' }),
156
+ label: p, key: 'ph-' + p, active: state.phase === p, href: '#' + p,
157
+ onClick: (e) => { e.preventDefault(); state.phase = p; kit.render(); }
158
+ })) },
90
159
  { group: 'jump', items: [
91
160
  { glyph: '·', label: 'tiles', key: 'j1', href: '#tiles' },
92
161
  { glyph: '·', label: 'swatches', key: 'j2', href: '#swatches' }
@@ -94,28 +163,38 @@ function App() {
94
163
  ]
95
164
  }),
96
165
  main: [
97
- h('div', { class: 'ds-section', style: 'padding:8px' },
166
+ h('div', { class: 'ds-section ds-section-pad' },
98
167
  Heading({ level: 1, children: 'gallery' }),
99
- Lede({ children: 'visual grid of tonal cards. tiles use the same panel tokens the rest of the system does — no bespoke tile component, no shadows, no borders.' }),
100
- Panel({ title: 'tiles', count: items.length, style: 'margin:8px 0', children:
101
- h('div', { style: 'padding:16px;display:grid;grid-template-columns:' + cols + ';gap:8px' }, ...items.map(Tile))
102
- }),
103
- Panel({ title: 'swatches', count: swatchTokens.length, style: 'margin:8px 0', children:
104
- h('div', { style: 'padding:16px;display:grid;grid-template-columns:repeat(auto-fill, minmax(160px, 1fr));gap:12px' }, ...swatchTokens.map(Swatch))
168
+ // Says what the reader is looking at. The old lede described
169
+ // the implementation ("no bespoke tile component, no shadows,
170
+ // no borders") and was contradicted on screen the tiles lift
171
+ // with a shadow on hover.
172
+ Lede({ children: 'twelve ascii specimens on one tonal frame. pick any tile to open it large.' }),
173
+ Panel({ title: 'tiles', count: state.phase === 'ready' ? items.length : 0, class: 'ds-panel-gap', children: TilesBody() }),
174
+ Panel({ title: 'swatches', count: swatchTokens.length, class: 'ds-panel-gap', children:
175
+ h('div', { class: 'ds-swatch-grid' }, ...swatchTokens.map(Swatch))
105
176
  }),
106
- Panel({ title: 'about this kit', style: 'margin:8px 0', children: h('div', { class: 'ds-pattern-notes' },
107
- h('p', {}, tiles are tonal panels stacked into a css grid — ', Chip({ tone: 'accent', children: 'auto-fill minmax' }), ' for the responsive default.'),
108
- h('p', {}, '· lightbox reuses the panel surface; no extra component, no transitions, no z-stack circus.'),
109
- h('p', {}, density toggle drops min tile size — same tokens, different rhythm.')
177
+ // Two notes that say something the page does not already show.
178
+ // The third ("density toggle drops min tile size") described a
179
+ // control the reader can just press, and the second claimed the
180
+ // lightbox has "no transitions" while .ds-gallery-tile animates
181
+ // transform and box-shadow on hover.
182
+ Panel({ title: 'about this kit', class: 'ds-panel-gap', children: h('div', { class: 'ds-pattern-notes' },
183
+ h('p', {}, '· the grid is ', Chip({ tone: 'accent', children: 'auto-fill minmax' }), ' — tiles reflow to the container, never to a breakpoint list.'),
184
+ h('p', {}, '· the lightbox is the same tonal panel at a larger size, so there is one surface to theme, not two.')
110
185
  ) })
111
186
  ),
112
187
  Lightbox()
113
188
  ],
114
189
  status: Status({
115
- left: ['gallery', ' ' + items.length + ' tiles', ' density=' + state.density],
190
+ left: ['gallery', '- ' + (state.phase === 'ready' ? items.length : 0) + ' tiles', '- density=' + state.density, '- ' + state.phase],
116
191
  right: ['247420 / mmxxvi']
117
192
  })
118
193
  });
119
194
  }
120
195
 
121
196
  const kit = mountKit({ root, view: App, screen: '14 Gallery' });
197
+
198
+ window.addEventListener('keydown', (e) => {
199
+ if (e.key === 'Escape' && state.open) { state.open = null; kit.render(); }
200
+ });
@@ -1,166 +1,184 @@
1
1
  import * as webjsx from 'webjsx';
2
2
  import { mountKit } from 'ds/bootstrap.js';
3
- const h = webjsx.createElement;
3
+ import {
4
+ AppShell, Topbar, Crumb, Status, Dot,
5
+ Hero, Panel, Row, Section, WorksList, WritingList, Manifesto
6
+ } from 'ds/components.js';
4
7
 
5
- const state = { route: 'works', opened: 0 };
8
+ const h = webjsx.createElement;
6
9
  const root = document.getElementById('root');
7
10
 
8
- const navItems = [['works','#works'],['writing','#writing'],['manifesto','#manifesto'],['source ↗','https://github.com/AnEntrypoint']];
11
+ const navItems = [
12
+ ['works', '#works'],
13
+ ['writing', '#writing'],
14
+ ['manifesto', '#manifesto'],
15
+ ['source ->', 'https://github.com/AnEntrypoint']
16
+ ];
17
+
18
+ const shipping = [
19
+ { name: 'gm', sub: 'state machine v0.4.1', live: true },
20
+ { name: 'zellous', sub: 'push-to-talk', live: true },
21
+ { name: 'thebird', sub: '—', live: false }
22
+ ];
23
+
24
+ // `href` (the "open ->" button) and `source` (the "source" button) are the two
25
+ // destinations WorksList renders inside an expanded work. Both are required:
26
+ // WorksList falls back to '#' when they are absent, which renders the buttons
27
+ // as real, pressable affordances that lead nowhere. Every entry below points at
28
+ // a repo that actually exists under the AnEntrypoint org.
29
+ const GH = 'https://github.com/AnEntrypoint/';
30
+ const works = [
31
+ { code: '001', title: 'gm', sub: 'state machine for coding agents', meta: '2025 · 3k', body: 'a tiny deterministic state machine that lets llms code without losing their minds. it thinks so you don\'t have to (as much).', href: GH + 'gm', source: GH + 'gm' },
32
+ { code: '002', title: 'zellous', sub: 'production push-to-talk', meta: '2024 · shipped', body: 'hold the button. talk. someone on the other side hears you. opus codec, dynamic rooms, 50-message replay.', href: GH + 'zellous', source: GH + 'zellous' },
33
+ { code: '003', title: 'spoint', sub: 'spawnpoint', meta: '2024 · shipped', body: 'the directory for "where should we start?" one url, one room, everyone lands in the same place.', href: GH + 'spoint', source: GH + 'spoint' },
34
+ { code: '004', title: 'flatspace', sub: 'flat-file cms', meta: 'wip', body: 'still figuring out what to say about this one. come back tuesday.', href: GH + 'flatspace', source: GH + 'flatspace' },
35
+ { code: '005', title: 'thebird', sub: '—', meta: 'wip', body: 'yes, the name is a reference. no, we won\'t tell you to what.', href: GH + 'thebird', source: GH + 'thebird' },
36
+ { code: '006', title: 'mcp-repl', sub: 'repl for mcp', meta: '2024 · live', body: 'executenodejs, executedeno, executebash, astgrep_search. if you don\'t know what those are, this one isn\'t for you.', href: GH + 'mcp-repl', source: GH + 'mcp-repl' },
37
+ { code: '007', title: 'mutagen', sub: 'adaptogen server', meta: '2024 · live', body: 'everything to do with a dapp deg3n. read the source.', href: GH + 'mutagen', source: GH + 'mutagen' },
38
+ { code: '008', title: 'techshaman', sub: 'member site', meta: 'ongoing', body: 'the official website for the techshaman. an entrypoint probably emerging.', href: GH + 'techshaman', source: GH + 'techshaman' }
39
+ ];
40
+
41
+ // There is no posts backend behind this kit, so these link to the blog kit —
42
+ // which renders the first entry below as a full post — rather than to invented
43
+ // per-post URLs that would 404. href:'#' (the previous value) rendered five
44
+ // anchors that looked like article links and did nothing when clicked.
45
+ const POST_HREF = '../blog/';
46
+ const posts = [
47
+ { date: '2026.04.14', title: 'we were here first', tag: 'lore', href: POST_HREF },
48
+ { date: '2026.03.22', title: 'gm v0.4 postmortem, or: why state machines', tag: 'gm', href: POST_HREF },
49
+ { date: '2026.02.09', title: 'push-to-talk is a protocol, not a feature', tag: 'zellous', href: POST_HREF },
50
+ { date: '2025.12.11', title: 'against the vibe-coded interface', tag: 'manifesto', href: POST_HREF },
51
+ { date: '2025.10.03', title: 'notes on shipping weird', tag: 'notes', href: POST_HREF }
52
+ ];
9
53
 
10
- function Topbar() {
11
- return h('header', { class: 'app-topbar' },
12
- h('span', { class: 'brand' }, '247420', h('span', { class: 'slash' }, ' / '), 'an entrypoint'),
13
- h('nav', {}, ...navItems.map(([label, href]) =>
14
- h('a', {
15
- key: label,
16
- href,
17
- class: state.route === label.replace(' ↗','') ? 'active' : '',
18
- onclick: (e) => { if (!href.startsWith('http')) { e.preventDefault(); state.route = label.replace(' ↗',''); render(); } }
19
- }, label)
54
+ const manifesto = [
55
+ { text: 'we are the creative department of the internet. always open (24/7). always a little bit high on possibility (420).' },
56
+ { text: 'move fast. break things. document honestly. ship the rough draft. humor is load-bearing.' },
57
+ { text: 'we will not tolerate simpleton design patterns, trifectas, gradients, or anything silly. nothing lame. we\'re internet natives and not easily pleased.', dim: true }
58
+ ];
59
+
60
+ // `phase` drives the three content lists (shipping / works / writing). Cycled
61
+ // from the topbar so each reading is reachable in the kit — a portfolio page
62
+ // that only ever renders a full grid has never shown what a cold or failed
63
+ // fetch looks like.
64
+ const state = { route: 'works', opened: 0, phase: 'ready' };
65
+ const PHASES = ['ready', 'loading', 'empty', 'error'];
66
+
67
+ // Row shimmer. Reuses .ds-event-row-skeleton + .ds-skel* (app-shell/files.css).
68
+ function ListSkeleton(n, prefix) {
69
+ return h('div', {},
70
+ ...Array.from({ length: n }, (_, i) => h('div', { key: prefix + i, class: 'ds-event-row-skeleton' },
71
+ h('span', { class: 'ds-skel ds-skel-rank' }),
72
+ h('span', { class: 'ds-skel ds-skel-title' }),
73
+ h('span', { class: 'ds-skel ds-skel-meta' })
20
74
  ))
21
75
  );
22
76
  }
23
77
 
24
- function Crumb() {
25
- return h('div', { class: 'app-crumb' },
26
- h('span', {}, '247420'), h('span', { class: 'sep' }, ''),
27
- h('span', { class: 'leaf' }, state.route)
78
+ function ListEmpty(msg, hint) {
79
+ return h('div', { class: 'ds-empty-state' },
80
+ h('div', { class: 'ds-empty-state-glyph' }, '[ ]'),
81
+ h('p', { class: 'ds-empty-state-msg' }, msg),
82
+ h('p', { class: 'ds-empty-state-hint' }, hint)
28
83
  );
29
84
  }
30
85
 
31
- function Hero() {
32
- return h('div', { style: 'padding:32px 32px 24px 32px' },
33
- h('h1', { style: 'font-size:36px;font-weight:600;margin:0 0 4px 0;color:var(--panel-text);letter-spacing:-0.01em' },
34
- 'the creative department of the internet.'
35
- ),
36
- h('p', { style: 'font-size:14px;line-height:1.55;color:var(--panel-text-2);max-width:64ch;margin:0 0 20px 0' },
37
- '247420 is a collective of mercurials. we ship fast, break things on purpose, and document honestly. ',
38
- h('span', { style: 'color:var(--panel-accent);font-weight:500' }, 'humor is load-bearing.')
39
- ),
40
- h('div', { class: 'panel', style: 'max-width:560px;margin:0' },
41
- h('div', { class: 'panel-head' }, h('span', {}, 'currently shipping'), h('span', {}, '3')),
42
- h('div', { class: 'panel-body' },
43
- h('div', { class: 'row' },
44
- h('span', { class: 'code' }, h('span', { style: 'color:var(--panel-accent)' }, '●')),
45
- h('span', { class: 'title' }, 'gm', h('span', { class: 'sub' }, 'state machine v0.4.1')),
46
- h('span', { class: 'meta' }, 'live')
47
- ),
48
- h('div', { class: 'row' },
49
- h('span', { class: 'code' }, h('span', { style: 'color:var(--panel-accent)' }, '●')),
50
- h('span', { class: 'title' }, 'zellous', h('span', { class: 'sub' }, 'push-to-talk')),
51
- h('span', { class: 'meta' }, 'live')
52
- ),
53
- h('div', { class: 'row' },
54
- h('span', { class: 'code' }, h('span', { style: 'color:var(--panel-text-3)' }, '○')),
55
- h('span', { class: 'title' }, 'thebird', h('span', { class: 'sub' }, '—')),
56
- h('span', { class: 'meta' }, 'wip')
57
- )
86
+ function ListError(title, msg) {
87
+ return h('div', { class: 'ds-alert ds-alert-error' },
88
+ h('span', { class: 'ds-alert-icon' }, '!'),
89
+ h('div', { class: 'ds-alert-content' },
90
+ h('div', { class: 'ds-alert-title' }, title),
91
+ h('div', { class: 'ds-alert-message' }, msg),
92
+ h('div', { class: 'ds-alert-retry' },
93
+ h('button', { class: 'btn', onclick: () => { state.phase = 'ready'; render(); } }, 'refetch')
58
94
  )
59
95
  )
60
96
  );
61
97
  }
62
98
 
63
- const works = [
64
- { code:'001', title:'gm', sub:'state machine for coding agents', meta:'2025 · 3k★', body:'a tiny deterministic state machine that lets llms code without losing their minds. it thinks so you don\'t have to (as much).' },
65
- { code:'002', title:'zellous', sub:'production push-to-talk', meta:'2024 · live', body:'hold the button. talk. someone on the other side hears you. opus codec, dynamic rooms, 50-message replay.' },
66
- { code:'003', title:'spoint', sub:'spawnpoint', meta:'2024 · live', body:'the directory for "where should we start?" one url, one room, everyone lands in the same place.' },
67
- { code:'004', title:'flatspace', sub:'flat-file cms', meta:'wip', body:'still figuring out what to say about this one. come back tuesday.' },
68
- { code:'005', title:'thebird', sub:'', meta:'wip', body:'yes, the name is a reference. no, we won\'t tell you to what.' },
69
- { code:'006', title:'mcp-repl', sub:'repl for mcp', meta:'2024 · live', body:'executenodejs, executedeno, executebash, astgrep_search. if you don\'t know what those are, this one isn\'t for you.' },
70
- { code:'007', title:'mutagen', sub:'adaptogen server', meta:'2024 · live', body:'everything to do with a dapp deg3n. read the source.' },
71
- { code:'008', title:'techshaman', sub:'member site', meta:'ongoing', body:'the official website for the techshaman. an entrypoint probably emerging.' }
72
- ];
73
-
74
- function Works() {
75
- return h('div', { style: 'padding:20px 32px' },
76
- h('h3', {}, '// works'),
77
- h('div', { class: 'panel' },
78
- h('div', { class: 'panel-head' },
79
- h('span', {}, 'works · 08 of ~61'),
80
- h('a', { href: 'https://github.com/AnEntrypoint', style: 'color:var(--panel-accent);text-decoration:none' }, 'all repos ↗')
81
- ),
82
- h('div', { class: 'panel-body' }, ...works.map((w, i) => {
83
- const isOpen = state.opened === i;
84
- return h('div', { key: i },
85
- h('div', {
86
- class: 'row' + (isOpen ? ' active' : ''),
87
- onclick: () => { state.opened = isOpen ? null : i; render(); }
88
- },
89
- h('span', { class: 'code' }, w.code),
90
- h('span', { class: 'title' }, w.title, h('span', { class: 'sub' }, w.sub)),
91
- h('span', { class: 'meta' }, w.meta + ' ' + (isOpen ? '−' : '+'))
92
- ),
93
- isOpen ? h('div', { class: 'work-detail' },
94
- h('p', { class: 'ds-prose ds-work-body' }, w.body),
95
- h('div', { class: 'ds-work-actions' },
96
- h('a', { class: 'btn-primary', href: '#' }, 'open ↗'),
97
- h('a', { class: 'btn', href: '#' }, 'source')
98
- )
99
- ) : null
100
- );
101
- }))
102
- )
103
- );
99
+ function ShippingBody() {
100
+ const p = state.phase;
101
+ if (p === 'loading') return ListSkeleton(3, 'sk-ship-');
102
+ if (p === 'error') return ListError('build status unavailable',
103
+ 'the status endpoint timed out, so live and wip cannot be told apart right now. the projects below are still real; only their badges are unknown.');
104
+ if (p === 'empty') return ListEmpty('nothing in flight this week',
105
+ 'projects appear here while they are actively being worked on. everything currently shipped is in works below.');
106
+ return h('div', {}, ...shipping.map((s) => Row({
107
+ key: s.name, leading: Dot({ tone: s.live ? 'on' : 'off' }),
108
+ title: s.name, sub: s.sub, meta: s.live ? 'live' : 'wip'
109
+ })));
104
110
  }
105
111
 
106
- function Writing() {
107
- const posts = [
108
- { date:'2026.04.14', title:'we were here first', tag:'lore' },
109
- { date:'2026.03.22', title:'gm v0.4 postmortem, or: why state machines', tag:'gm' },
110
- { date:'2026.02.09', title:'push-to-talk is a protocol, not a feature', tag:'zellous' },
111
- { date:'2025.12.11', title:'against the vibe-coded interface', tag:'manifesto' },
112
- { date:'2025.10.03', title:'notes on shipping weird', tag:'notes' }
113
- ];
114
- return h('div', { style: 'padding:20px 32px' },
115
- h('h3', {}, '// recent writing'),
116
- h('div', { class: 'panel' },
117
- h('div', { class: 'panel-body' }, ...posts.map((p, i) =>
118
- h('a', { key: i, class: 'row', href: '#' },
119
- h('span', { class: 'code' }, p.date),
120
- h('span', { class: 'title' }, p.title),
121
- h('span', { class: 'meta' }, '§ ' + p.tag)
122
- )
123
- ))
124
- )
125
- );
112
+ function WorksBody() {
113
+ const p = state.phase;
114
+ if (p === 'loading') return ListSkeleton(6, 'sk-work-');
115
+ if (p === 'error') return ListError('works index failed to load',
116
+ 'the projects manifest returned malformed json at entry 4, so the list was rejected rather than shown with a hole in it. a refetch usually picks up the corrected file.');
117
+ if (p === 'empty') return ListEmpty('no works published yet',
118
+ 'each entry is one shipped project with its year, size and a paragraph on what it does. the first one lands here as soon as it is tagged.');
119
+ return WorksList({ works, openedIndex: state.opened, onToggle: (i) => { state.opened = i; render(); } });
126
120
  }
127
121
 
128
- function Manifesto() {
129
- return h('div', { class: 'ds-section ds-manifesto-section' },
130
- h('h3', {}, '// manifesto · rough draft'),
131
- h('div', { class: 'ds-prose ds-manifesto' },
132
- h('p', { class: 'ds-manifesto-para' }, 'we are the creative department of the internet. always open (24/7). always a little bit high on possibility (420).'),
133
- h('p', { class: 'ds-manifesto-para' }, 'move fast. break things. document honestly. ship the rough draft. ', h('strong', {}, 'humor is load-bearing.')),
134
- h('p', { class: 'ds-manifesto-para dim' }, 'we will not tolerate simpleton design patterns, trifectas, gradients, or anything silly. nothing lame. we\'re internet natives and not easily pleased.')
135
- )
136
- );
137
- }
138
-
139
- function Status() {
140
- return h('footer', { class: 'app-status' },
141
- h('span', { class: 'item' }, 'main'),
142
- h('span', { class: 'item' }, '• 8 works'),
143
- h('span', { class: 'item' }, '• 5 posts'),
144
- h('span', { class: 'spread' }),
145
- h('span', { class: 'item' }, 'probably emerging'),
146
- h('span', { class: 'item' }, h('a', { href: 'https://github.com/AnEntrypoint' }, 'source ↗'))
147
- );
122
+ function WritingBody() {
123
+ const p = state.phase;
124
+ if (p === 'loading') return ListSkeleton(5, 'sk-post-');
125
+ if (p === 'error') return ListError('writing feed unreachable',
126
+ 'the posts feed is served from a different origin and that origin is down. the works list above is local and unaffected.');
127
+ if (p === 'empty') return ListEmpty('nothing written lately',
128
+ 'posts land here newest first, tagged by which project they belong to. quiet here usually means loud somewhere else.');
129
+ return WritingList({ posts });
148
130
  }
149
131
 
150
132
  function App() {
151
- return h('div', { class: 'app' },
152
- Topbar(),
153
- Crumb(),
154
- h('div', { class: 'app-body no-side' },
155
- h('main', { class: 'app-main', style: 'padding:0' },
156
- Hero(),
157
- Works(),
158
- Writing(),
159
- Manifesto()
160
- )
161
- ),
162
- Status()
163
- );
133
+ return AppShell({
134
+ // The nav carries destinations only. The phase cycler used to ride
135
+ // here as a fifth item, which made a demo control a peer of works /
136
+ // writing / manifesto and let it take the `active` highlight away from
137
+ // the section the reader is actually in. It moved onto the first panel
138
+ // it governs.
139
+ topbar: Topbar({
140
+ brand: '247420', leaf: 'an entrypoint',
141
+ items: navItems,
142
+ active: state.route,
143
+ onNav: (label) => { state.route = label; render(); }
144
+ }),
145
+ crumb: Crumb({ trail: ['247420'], leaf: state.route }),
146
+ main: [
147
+ // badges fill the Hero's narrow right column — without them the
148
+ // asymmetric two-column grid renders with a dead right half at
149
+ // desktop width, which reads as a layout bug rather than tension.
150
+ // They are counts this page already knows, not invented metrics.
151
+ Hero({
152
+ title: 'the creative department of the internet.',
153
+ body: '247420 is a collective of mercurials. we ship fast, break things on purpose, and document honestly.',
154
+ accent: 'humor is load-bearing.',
155
+ badges: state.phase === 'ready'
156
+ ? [works.length + ' works', posts.length + ' posts', shipping.filter((s) => s.live).length + ' live']
157
+ : null
158
+ }),
159
+ Panel({
160
+ title: 'currently shipping',
161
+ right: h('button', { class: 'btn', onclick: () => {
162
+ state.phase = PHASES[(PHASES.indexOf(state.phase) + 1) % PHASES.length];
163
+ render();
164
+ } }, state.phase === 'ready' ? shipping.length + ' in flight' : state.phase),
165
+ children: ShippingBody()
166
+ }),
167
+ // No eyebrow: '08 of ~61' was a position indicator over a section
168
+ // that is not part of any sequence, and the ~61 contradicted the
169
+ // eight works actually listed. The count belongs in the status bar,
170
+ // which already carries it.
171
+ Section({ id: 'works', title: 'works', children: WorksBody() }),
172
+ Section({ id: 'writing', title: 'recent writing',
173
+ children: WritingBody() }),
174
+ Section({ id: 'manifesto', title: 'manifesto · rough draft',
175
+ children: Manifesto({ paragraphs: manifesto }) })
176
+ ],
177
+ status: Status({
178
+ left: ['main', state.phase === 'ready' ? '8 works' : '0 works', state.phase === 'ready' ? '5 posts' : '0 posts', state.phase],
179
+ right: ['probably emerging', h('a', { href: 'https://github.com/AnEntrypoint' }, 'source ->')]
180
+ })
181
+ });
164
182
  }
165
183
 
166
184
  const kit = mountKit({ root, view: App, screen: '01 Homepage' });