Implement core value modals

This commit is contained in:
adilallo
2026-04-15 23:13:28 -06:00
parent beae150f02
commit eedb70f9f3
15 changed files with 806 additions and 101 deletions
@@ -25,6 +25,7 @@ const CreateContainer = memo<CreateProps>(
className = "",
ariaLabel,
ariaLabelledBy,
backdropVariant = "default",
}) => {
const createRef = useRef<HTMLDivElement>(null);
const overlayRef = useRef<HTMLDivElement>(null);
@@ -132,6 +133,7 @@ const CreateContainer = memo<CreateProps>(
ariaLabelledBy={ariaLabelledBy}
createRef={createRef}
overlayRef={overlayRef}
backdropVariant={backdropVariant}
/>
);
},
@@ -27,6 +27,11 @@ export interface CreateProps {
multiSelect?: boolean;
upload?: boolean;
proportion?: boolean;
/**
* Backdrop behind the dialog. `loginYellow` matches the Login modals blurred brand overlay.
* @default "default"
*/
backdropVariant?: "default" | "loginYellow";
}
export interface CreateViewProps {
@@ -51,4 +56,5 @@ export interface CreateViewProps {
ariaLabelledBy?: string;
createRef: React.RefObject<HTMLDivElement>;
overlayRef: React.RefObject<HTMLDivElement>;
backdropVariant: "default" | "loginYellow";
}
+11 -1
View File
@@ -6,6 +6,15 @@ import ModalFooter from "../../utility/ModalFooter";
import ModalHeader from "../../utility/ModalHeader";
import type { CreateViewProps } from "./Create.types";
const backdropOverlayClasses: Record<
CreateViewProps["backdropVariant"],
string
> = {
default: "fixed inset-0 bg-black/50 z-[9998]",
loginYellow:
"fixed inset-0 z-[9998] bg-[var(--color-surface-inverse-brand-primary)]/85 backdrop-blur-md supports-[backdrop-filter]:bg-[var(--color-surface-inverse-brand-primary)]/75",
};
export function CreateView({
isOpen,
onClose,
@@ -28,6 +37,7 @@ export function CreateView({
ariaLabelledBy,
createRef,
overlayRef,
backdropVariant,
}: CreateViewProps) {
if (!isOpen) return null;
@@ -36,7 +46,7 @@ export function CreateView({
{/* Overlay */}
<div
ref={overlayRef}
className="fixed inset-0 bg-black/50 z-[9998]"
className={backdropOverlayClasses[backdropVariant]}
onClick={onClose}
aria-hidden="true"
/>
+31 -3
View File
@@ -20,6 +20,11 @@ import {
readAnonymousCreateFlowState,
writeAnonymousCreateFlowState,
} from "../utils/anonymousDraftStorage";
import {
clearCoreValueDetailsLocalStorage,
readCoreValueDetailsFromLocalStorage,
writeCoreValueDetailsToLocalStorage,
} from "../utils/coreValueDetailsLocalStorage";
const CreateFlowContext = createContext<CreateFlowContextValue | null>(null);
@@ -41,9 +46,20 @@ export function CreateFlowProvider({
initialStep = null,
enableAnonymousPersistence = false,
}: CreateFlowProviderProps) {
const [state, setState] = useState<CreateFlowState>(() =>
enableAnonymousPersistence ? readAnonymousCreateFlowState() : {},
);
const [state, setState] = useState<CreateFlowState>(() => {
const base = enableAnonymousPersistence
? readAnonymousCreateFlowState()
: {};
const storedDetails = readCoreValueDetailsFromLocalStorage();
if (Object.keys(storedDetails).length === 0) return base;
return {
...base,
coreValueDetailsByChipId: {
...storedDetails,
...(base.coreValueDetailsByChipId ?? {}),
},
};
});
const [interactionTouched, setInteractionTouched] = useState(false);
const [currentStep] = useState<CreateFlowStep | null>(initialStep);
const prevPersistRef = useRef(enableAnonymousPersistence);
@@ -72,6 +88,11 @@ export function CreateFlowProvider({
writeAnonymousCreateFlowState(state);
}, [state, enableAnonymousPersistence]);
/** Meaning/signals for core values: survives refresh for signed-in users; merged with anonymous draft when both exist. */
useEffect(() => {
writeCoreValueDetailsToLocalStorage(state.coreValueDetailsByChipId);
}, [state.coreValueDetailsByChipId]);
const markCreateFlowInteraction = useCallback(() => {
setInteractionTouched(true);
}, []);
@@ -85,6 +106,12 @@ export function CreateFlowProvider({
...updates.communityStructureChipSnapshots,
};
}
if (updates.coreValueDetailsByChipId !== undefined) {
merged.coreValueDetailsByChipId = {
...(prevState.coreValueDetailsByChipId ?? {}),
...updates.coreValueDetailsByChipId,
};
}
return merged;
});
}, []);
@@ -97,6 +124,7 @@ export function CreateFlowProvider({
setState({});
setInteractionTouched(false);
clearAnonymousCreateFlowStorage();
clearCoreValueDetailsLocalStorage();
}, []);
const contextValue: CreateFlowContextValue = {
@@ -1,8 +1,11 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, useMemo } from "react";
import MultiSelect from "../../../components/controls/MultiSelect";
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
import TextArea from "../../../components/controls/TextArea";
import Create from "../../../components/modals/Create";
import ContentLockup from "../../../components/type/ContentLockup";
import { useMessages } from "../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import type { CommunityStructureChipSnapshotRow } from "../../types";
@@ -11,10 +14,38 @@ import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoCo
const MAX_CORE_VALUES = 5;
function chipRowsFromLabels(rows: readonly string[]): ChipOption[] {
return rows.map((label, i) => ({
type ModalSession = "pending" | "editing";
/** Row in `coreValues.json` `values` — string (legacy) or `{ label, meaning, signals }`. */
type CoreValuePresetJson =
| string
| { label: string; meaning?: string; signals?: string };
type CoreValuePreset = {
label: string;
meaning: string;
signals: string;
};
function normalizeCoreValuePresets(
values: readonly CoreValuePresetJson[],
): CoreValuePreset[] {
return values.map((v) => {
if (typeof v === "string") {
return { label: v, meaning: "", signals: "" };
}
return {
label: v.label,
meaning: typeof v.meaning === "string" ? v.meaning : "",
signals: typeof v.signals === "string" ? v.signals : "",
};
});
}
function chipRowsFromPresets(presets: readonly CoreValuePreset[]): ChipOption[] {
return presets.map((row, i) => ({
id: String(i + 1),
label,
label: row.label,
state: "Unselected" as const,
}));
}
@@ -69,7 +100,11 @@ function snapshotRowsToChipOptions(
export function CoreValuesSelectScreen() {
const m = useMessages();
const cv = m.create.coreValues;
const presetLabels = cv.values;
const presets = useMemo(
() => normalizeCoreValuePresets(cv.values as CoreValuePresetJson[]),
[cv.values],
);
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
const [coreValueOptions, setCoreValueOptions] = useState<ChipOption[]>(
@@ -77,12 +112,19 @@ export function CoreValuesSelectScreen() {
const fromSnap = snapshotRowsToChipOptions(state.coreValuesChipsSnapshot);
if (fromSnap) return fromSnap;
return applySavedSelection(
chipRowsFromLabels(presetLabels),
chipRowsFromPresets(presets),
state.selectedCoreValueIds,
);
},
);
const [activeModalChipId, setActiveModalChipId] = useState<string | null>(
null,
);
const [modalSession, setModalSession] = useState<ModalSession | null>(null);
const [draftMeaning, setDraftMeaning] = useState("");
const [draftSignals, setDraftSignals] = useState("");
useEffect(() => {
const fromSnap = snapshotRowsToChipOptions(state.coreValuesChipsSnapshot);
if (fromSnap) {
@@ -94,40 +136,121 @@ export function CoreValuesSelectScreen() {
);
}, [state.coreValuesChipsSnapshot, state.selectedCoreValueIds]);
const persistCoreValues = useCallback(
/** Sync chips to create-flow draft. Never call `updateState` from inside a `setCoreValueOptions` updater — defer with `queueMicrotask`. */
const syncCoreValuesToDraft = useCallback(
(next: ChipOption[]) => {
markCreateFlowInteraction();
setCoreValueOptions(next);
updateState({
selectedCoreValueIds: selectedIdsFromOptions(next),
coreValuesChipsSnapshot: chipOptionsToSnapshotRows(next),
});
},
[markCreateFlowInteraction, updateState],
[updateState],
);
const persistCoreValues = useCallback(
(next: ChipOption[]) => {
markCreateFlowInteraction();
setCoreValueOptions(next);
syncCoreValuesToDraft(next);
},
[markCreateFlowInteraction, syncCoreValuesToDraft],
);
/** Default meaning/signals from `coreValues.json` `values` for each preset label. */
const getPresetTexts = useCallback(
(valueLabel: string): { meaning: string; signals: string } => {
const row = presets.find((p) => p.label === valueLabel);
if (!row) return { meaning: "", signals: "" };
return { meaning: row.meaning, signals: row.signals };
},
[presets],
);
const getInitialTexts = useCallback(
(chipId: string, valueLabel: string) => {
const saved = state.coreValueDetailsByChipId?.[chipId];
const preset = getPresetTexts(valueLabel);
return {
meaning: saved?.meaning ?? preset.meaning,
signals: saved?.signals ?? preset.signals,
};
},
[state.coreValueDetailsByChipId, getPresetTexts],
);
const openModal = useCallback(
(chipId: string, session: ModalSession, valueLabel: string) => {
const initial = getInitialTexts(chipId, valueLabel);
setDraftMeaning(initial.meaning);
setDraftSignals(initial.signals);
setActiveModalChipId(chipId);
setModalSession(session);
markCreateFlowInteraction();
},
[getInitialTexts, markCreateFlowInteraction],
);
const handleModalDismiss = useCallback(() => {
if (activeModalChipId && modalSession === "pending") {
const next = coreValueOptions.map((opt) =>
opt.id === activeModalChipId
? { ...opt, state: "Unselected" as const }
: opt,
);
persistCoreValues(next);
}
setActiveModalChipId(null);
setModalSession(null);
}, [activeModalChipId, modalSession, coreValueOptions, persistCoreValues]);
const handleModalConfirm = useCallback(() => {
if (!activeModalChipId) return;
markCreateFlowInteraction();
updateState({
coreValueDetailsByChipId: {
[activeModalChipId]: {
meaning: draftMeaning,
signals: draftSignals,
},
},
});
setActiveModalChipId(null);
setModalSession(null);
}, [
activeModalChipId,
draftMeaning,
draftSignals,
markCreateFlowInteraction,
updateState,
]);
const handleChipClick = (chipId: string) => {
const target = coreValueOptions.find((o) => o.id === chipId);
if (!target || target.state === "Custom") return;
const willSelect = target.state !== "Selected";
const selectedCount = coreValueOptions.filter(
(o) => o.state === "Selected",
).length;
if (willSelect && selectedCount >= MAX_CORE_VALUES) return;
if (target.state === "Selected") {
const next: ChipOption[] = coreValueOptions.map((opt) =>
opt.id === chipId
? { ...opt, state: "Unselected" as const }
: opt,
);
persistCoreValues(next);
return;
}
if (selectedCount >= MAX_CORE_VALUES) return;
const next: ChipOption[] = coreValueOptions.map((opt) =>
opt.id === chipId
? {
...opt,
state:
opt.state === "Selected"
? ("Unselected" as const)
: ("Selected" as const),
}
? { ...opt, state: "Selected" as const }
: opt,
);
persistCoreValues(next);
openModal(chipId, "pending", target.label);
};
const addHandlers = {
@@ -138,24 +261,37 @@ export function CoreValuesSelectScreen() {
...prev,
{ id: crypto.randomUUID(), label: "", state: "Custom" },
];
updateState({
selectedCoreValueIds: selectedIdsFromOptions(next),
coreValuesChipsSnapshot: chipOptionsToSnapshotRows(next),
});
queueMicrotask(() => syncCoreValuesToDraft(next));
return next;
});
},
onCustomChipConfirm: (chipId: string, value: string) => {
markCreateFlowInteraction();
setCoreValueOptions((prev) => {
const next = prev.map((opt) =>
const withLabel = prev.map((opt) =>
opt.id === chipId
? { ...opt, label: value, state: "Unselected" as const }
: opt,
);
updateState({
selectedCoreValueIds: selectedIdsFromOptions(next),
coreValuesChipsSnapshot: chipOptionsToSnapshotRows(next),
const selectedCount = withLabel.filter(
(o) => o.state === "Selected",
).length;
const canSelect = selectedCount < MAX_CORE_VALUES;
const next = canSelect
? withLabel.map((opt) =>
opt.id === chipId
? { ...opt, state: "Selected" as const }
: opt,
)
: withLabel;
queueMicrotask(() => {
syncCoreValuesToDraft(next);
if (canSelect) {
openModal(chipId, "pending", value);
} else {
openModal(chipId, "editing", value);
}
});
return next;
});
@@ -164,15 +300,15 @@ export function CoreValuesSelectScreen() {
markCreateFlowInteraction();
setCoreValueOptions((prev) => {
const next = prev.filter((o) => o.id !== chipId);
updateState({
selectedCoreValueIds: selectedIdsFromOptions(next),
coreValuesChipsSnapshot: chipOptionsToSnapshotRows(next),
});
queueMicrotask(() => syncCoreValuesToDraft(next));
return next;
});
},
};
const modalChipLabel =
coreValueOptions.find((o) => o.id === activeModalChipId)?.label ?? "";
const description = (
<>
<span className="leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
@@ -192,6 +328,8 @@ export function CoreValuesSelectScreen() {
</>
);
const detailModal = cv.detailModal;
return (
<CreateFlowTwoColumnSelectShell
lgVerticalAlign="start"
@@ -214,6 +352,50 @@ export function CoreValuesSelectScreen() {
addButton
addButtonText={cv.multiSelect.addButtonText}
/>
{detailModal && (
<Create
isOpen={activeModalChipId !== null}
onClose={handleModalDismiss}
backdropVariant="loginYellow"
headerContent={
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
<ContentLockup
title={modalChipLabel}
description={detailModal.subtitle}
variant="modal"
alignment="left"
/>
</div>
}
showBackButton={false}
showNextButton
onNext={handleModalConfirm}
nextButtonText={detailModal.addValueButton}
ariaLabel={modalChipLabel || "Core value details"}
>
<div className="flex flex-col gap-[var(--measures-spacing-600,24px)] pb-2">
<TextArea
label={detailModal.meaningLabel}
showHelpIcon
appearance="embedded"
size="medium"
value={draftMeaning}
onChange={(e) => setDraftMeaning(e.target.value)}
rows={4}
/>
<TextArea
label={detailModal.signalsLabel}
showHelpIcon
appearance="embedded"
size="medium"
value={draftSignals}
onChange={(e) => setDraftSignals(e.target.value)}
rows={4}
/>
</div>
</Create>
)}
</CreateFlowTwoColumnSelectShell>
);
}
+8
View File
@@ -42,6 +42,12 @@ export type CommunityStructureChipSnapshotRow = {
state?: string;
};
/** Meaning + violation signals copy for a core value chip (draft + publish). */
export type CoreValueDetailEntry = {
meaning: string;
signals: string;
};
/**
* Flow state for inputs across create-flow steps.
* Validated on `PUT /api/drafts/me` via `createFlowStateSchema` (Zod + JSON safety checks).
@@ -75,6 +81,8 @@ export interface CreateFlowState {
selectedCoreValueIds?: string[];
/** Full chip rows for core values (custom labels). */
coreValuesChipsSnapshot?: CommunityStructureChipSnapshotRow[];
/** User-authored detail text keyed by chip id (preset ids or custom UUIDs). */
coreValueDetailsByChipId?: Record<string, CoreValueDetailEntry>;
currentStep?: CreateFlowStep;
/** Section drafts; structure will tighten as steps persist real shapes. */
sections?: Record<string, unknown>[];
@@ -0,0 +1,57 @@
import type { CoreValueDetailEntry } from "../types";
/** Persists meaning/signals per chip id across refresh (esp. signed-in create flow, in-memory only). */
export const CORE_VALUE_DETAILS_STORAGE_KEY =
"create-flow-core-value-details" as const;
export function readCoreValueDetailsFromLocalStorage(): Record<
string,
CoreValueDetailEntry
> {
if (typeof window === "undefined") return {};
try {
const raw = window.localStorage.getItem(CORE_VALUE_DETAILS_STORAGE_KEY);
if (!raw) return {};
const parsed = JSON.parse(raw) as unknown;
if (!parsed || typeof parsed !== "object") return {};
const out: Record<string, CoreValueDetailEntry> = {};
for (const [k, v] of Object.entries(parsed)) {
if (!v || typeof v !== "object") continue;
const o = v as Record<string, unknown>;
if (typeof o.meaning !== "string" || typeof o.signals !== "string") {
continue;
}
out[k] = { meaning: o.meaning, signals: o.signals };
}
return out;
} catch {
return {};
}
}
export function writeCoreValueDetailsToLocalStorage(
value: Record<string, CoreValueDetailEntry> | undefined,
): void {
if (typeof window === "undefined") return;
try {
if (!value || Object.keys(value).length === 0) {
window.localStorage.removeItem(CORE_VALUE_DETAILS_STORAGE_KEY);
return;
}
window.localStorage.setItem(
CORE_VALUE_DETAILS_STORAGE_KEY,
JSON.stringify(value),
);
} catch {
// quota / private mode
}
}
export function clearCoreValueDetailsLocalStorage(): void {
if (typeof window === "undefined") return;
try {
window.localStorage.removeItem(CORE_VALUE_DETAILS_STORAGE_KEY);
} catch {
// ignore
}
}
+34 -3
View File
@@ -1,4 +1,4 @@
import type { CreateFlowState } from "../../app/create/types";
import type { CoreValueDetailEntry, CreateFlowState } from "../../app/create/types";
import type { CommunityRuleDocumentSection } from "../../app/components/sections/CommunityRuleDocument/CommunityRuleDocument.types";
function isDocumentEntry(x: unknown): x is { title: string; body: string } {
@@ -28,6 +28,30 @@ export function parseSectionsFromCreateFlowState(
return out;
}
/** Core values selected in the flow with labels and detail text for the published document. */
export function buildCoreValuesForDocument(state: CreateFlowState): Array<{
chipId: string;
label: string;
meaning: string;
signals: string;
}> {
const snap = state.coreValuesChipsSnapshot;
const selected = new Set(state.selectedCoreValueIds ?? []);
const details = state.coreValueDetailsByChipId ?? {};
if (!snap?.length) return [];
return snap
.filter((r) => selected.has(r.id))
.map((r) => {
const d: CoreValueDetailEntry | undefined = details[r.id];
return {
chipId: r.id,
label: r.label,
meaning: d?.meaning ?? "",
signals: d?.signals ?? "",
};
});
}
export type BuildPublishPayloadResult =
| {
ok: true;
@@ -72,10 +96,17 @@ export function buildPublishPayload(
];
}
const coreValues = buildCoreValuesForDocument(state);
if (summary !== undefined) {
return { ok: true, title, summary, document: { sections } };
return {
ok: true,
title,
summary,
document: { sections, coreValues },
};
}
return { ok: true, title, document: { sections } };
return { ok: true, title, document: { sections, coreValues } };
}
/** Read `document.sections` from a stored published payload for display. */
@@ -20,6 +20,11 @@ const communityStructureChipSnapshotsSchema = z
})
.strict();
const coreValueDetailEntrySchema = z.object({
meaning: z.string().max(8000),
signals: z.string().max(8000),
});
/**
* Published rule `document` column: arbitrary JSON object with safety bounds.
*/
@@ -55,6 +60,9 @@ export const createFlowStateSchema = z
coreValuesChipsSnapshot: z
.array(communityStructureChipSnapshotRowSchema)
.optional(),
coreValueDetailsByChipId: z
.record(coreValueDetailEntrySchema)
.optional(),
currentStep: createFlowStepSchema.optional(),
sections: z.array(z.unknown()).optional(),
stakeholders: z.array(z.unknown()).optional(),
+316 -62
View File
@@ -8,68 +8,322 @@
"multiSelect": {
"addButtonText": "Add value"
},
"detailModal": {
"subtitle": "Edit or add to this description to describe what this value means to your community.",
"meaningLabel": "What does this value mean to your group?",
"signalsLabel": "Signals of Violation",
"addValueButton": "Add Value"
},
"values": [
"Accessibility",
"Accountability",
"Adaptability",
"Agency",
"Altruism",
"Anti-oppression",
"Autonomy",
"Capacity Building",
"Collaboration",
"Common Ownership",
"Community Care",
"Conflict Resolution",
"Consent",
"Consensus",
"Constructive Feedback",
"Cooperation",
"Copyleft",
"Decentralization",
"Direct Action",
"Diversity",
"Documentation",
"Education",
"Empathy",
"Empowerment",
"Equity",
"Experimentation",
"Fairness",
"Fair Remuneration",
"Flexibility",
"Forkability",
"Freedom",
"Freedom of Information",
"Generosity",
"Harm Reduction",
"Holism",
"Holocracy",
"Honesty",
"Horizontalism",
"Humility",
"Inclusion",
"Inclusivity",
"Independence",
"Innovation",
"Integrity",
"Interdependence",
"Interoperability",
"Intersectionality",
"Justice",
"Knowledge Sharing",
"Labor Rights",
"Leadership",
"Learning",
"Liberty",
"Localism",
"Maintenance",
"Mentorship",
"Meritocracy",
"Mutual Aid",
"Non-violence",
"Open Source",
"Openness",
"Participation"
{
"label": "Accessibility",
"meaning": "We design spaces and tools that everyone can use regardless of ability or background. To us, this means we provide alt-text for images, wheelchair ramps for events, and use plain language in documents. We acknowledge a tension with Aesthetics/Security (Simple design vs flashy easy access vs strict gates).",
"signals": "Hosting events in venues that are physically inaccessible to wheelchair users or publishing video content without captions which excludes the deaf community."
},
{
"label": "Accountability",
"meaning": "Members accept responsibility for their actions and their impact on the community. To us, this means we publish error logs and hold retrospective meetings to own our mistakes without shifting blame. We acknowledge a tension with Psychological Safety (Blame-free culture vs owning mistakes).",
"signals": "Leaders refusing to accept responsibility for failures and instead blaming external factors or junior team members for the lack of success."
},
{
"label": "Adaptability",
"meaning": "The group remains flexible and willing to change strategies when circumstances shift. To us, this means we pivot strategies quarterly based on feedback rather than sticking to a rigid 5-year plan. We acknowledge a tension with Consistency (Changing course vs staying the path).",
"signals": "Persisting with a strategy that evidence shows is failing or penalizing members who suggest necessary changes based on new data."
},
{
"label": "Agency",
"meaning": "Every individual possesses the power and capacity to act on their own behalf. To us, this means Members choose their own tasks and define their own working hours. We acknowledge a tension with Alignment (Doing what you want vs pulling together).",
"signals": "Managers micromanaging details that should be up to the individual or requiring approval for minor decisions that slows down work unnecessarily."
},
{
"label": "Altruism",
"meaning": "We act out of a genuine concern for the well-being of others without expecting reward. To us, this means we offer services for free or pay-what-you-can, prioritizing need over ability to pay. We acknowledge a tension with Sustainability (Giving it all away vs keeping the lights on).",
"signals": "Only offering assistance when it provides a public relations benefit or expecting recipients of aid to perform public gratitude."
},
{
"label": "Anti-oppression",
"meaning": "We actively oppose all forms of systemic discrimination and hierarchy. To us, this means we have a dedicated diversity officer and mandatory bias training. We acknowledge a tension with Comfort (Challenging norms is uncomfortable).",
"signals": "Dismissing concerns about bias as irrelevant politics or refusing to examine how organizational structures replicate systemic inequalities."
},
{
"label": "Autonomy",
"meaning": "The group maintains independence from external control by governments or corporations. To us, this means we refuse funding that comes with strings attached or veto power. We acknowledge a tension with Coordination (Independence vs standardization).",
"signals": "Accepting funding that comes with strings attached such as donor veto power or allowing external partners to dictate internal strategy."
},
{
"label": "Capacity Building",
"meaning": "We focus on teaching skills so that members become stronger and more capable over time. To us, this means we dedicate 20% of the budget to training and mentorship programs. We acknowledge a tension with Efficiency (Training takes time away from doing).",
"signals": "Cutting the training budget immediately when funds are tight or expecting junior members to perform without providing any mentorship."
},
{
"label": "Collaboration",
"meaning": "We work together to solve problems rather than competing against one another. To us, this means we default to 'Multi-player mode' on documents and credit all contributors. We acknowledge a tension with Speed (Group work is slower than solo work).",
"signals": "Individuals hoarding information to make themselves indispensable or taking sole credit for work that was the result of a team effort."
},
{
"label": "Common Ownership",
"meaning": "Resources and assets are owned collectively by the members rather than by private investors. To us, this means Assets are held in a trust or cooperative structure where every member has a share. We acknowledge a tension with Investment Speed (Harder to raise VC capital).",
"signals": "Structuring the organization so that a small group of founders retains all voting power and profits despite claims of being a collective."
},
{
"label": "Community Care",
"meaning": "We prioritize the physical and mental health of our members above productivity. To us, this means we start meetings with check-ins and fund mental health stipends. We acknowledge a tension with Productivity (Rest takes time away from output).",
"signals": "Praising individuals for working while sick or scheduling mandatory meetings during times that are ostensibly reserved for rest and recovery."
},
{
"label": "Conflict Resolution",
"meaning": "We address disagreements openly and constructively to find a path forward. To us, this means we have a standing mediation committee and clear grievance forms. We acknowledge a tension with Harmony (Addressing conflict feels disruptive).",
"signals": "Ignoring interpersonal disputes in the hope they will disappear or labeling members who raise valid concerns as troublemakers to silence them."
},
{
"label": "Consent",
"meaning": "Participation and interactions are always voluntary and based on clear permission. To us, this means we use 'opt-in' by default for all communications and initiatives. We acknowledge a tension with Speed (Asking everyone takes time).",
"signals": "Automatically subscribing members to lists without asking or pressuring individuals to take on tasks they have already explicitly declined."
},
{
"label": "Consensus",
"meaning": "We strive to make decisions that everyone can agree to or at least live with. To us, this means we do not move forward with a proposal until all blocking concerns are resolved. We acknowledge a tension with Action/Speed (Everyone agreeing takes forever).",
"signals": "Pushing a decision forward despite unresolved blocking concerns or using social pressure to force dissenters to conform to the majority."
},
{
"label": "Constructive Feedback",
"meaning": "Criticism is offered kindly to help improve the work rather than to tear people down. To us, this means we use the 'Praise-Improve-Praise' sandwich and focus on the work, not the person. We acknowledge a tension with Politeness (Honesty can hurt feelings).",
"signals": "Delivering harsh criticism without offering actionable advice for improvement or attacking a person's character rather than their work."
},
{
"label": "Cooperation",
"meaning": "We believe working together produces better results than working in isolation. To us, this means we actively partner with similar organizations instead of competing for market share. We acknowledge a tension with Competition (Sharing the pie vs taking the pie).",
"signals": "Viewing similar organizations as threats to be destroyed rather than partners or refusing to share resources that could help the broader ecosystem."
},
{
"label": "Copyleft",
"meaning": "We use licensing that ensures derived works remain free and open for everyone. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Releasing modifications to open projects under restrictive licenses that prevent the community from benefiting from the improvements."
},
{
"label": "Decentralization",
"meaning": "Power is distributed across a network rather than concentrated in a central authority. To us, this means Decision-making authority is pushed to the edges no central HQ exists. We acknowledge a tension with Efficiency/Cohesion (Herding cats).",
"signals": "Concentrating all critical decision-making power in a single founder or small inner circle while claiming to be a flat organization."
},
{
"label": "Direct Action",
"meaning": "We take immediate steps to solve problems ourselves instead of asking authorities to do it. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Forming a committee to study a problem endlessly instead of taking immediate steps to address the urgent needs of the community."
},
{
"label": "Diversity",
"meaning": "We value a wide range of perspectives and backgrounds within our membership. To us, this means we strictly enforce quotas or targets for representation in leadership. We acknowledge a tension with Homophily/Ease (It's easier to work with people like you).",
"signals": "Hiring primarily for culture fit which results in a homogenous team or having a leadership team that lacks representation from marginalized groups."
},
{
"label": "Documentation",
"meaning": "We record our processes clearly so that others can learn from and duplicate our work. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Relying on oral tradition where only long-time members understand the systems or refusing to write down processes because it takes too much time."
},
{
"label": "Education",
"meaning": "We are committed to the continuous learning and growth of all members. To us, this means we publish open curriculums and host free workshops for the public. We acknowledge a tension with Action (Learning vs Doing).",
"signals": "Treating questions from newcomers as annoyances or creating an environment where members are afraid to admit they do not know something."
},
{
"label": "Empathy",
"meaning": "We strive to understand and share the feelings of others in our community. To us, this means we prioritize user research and listening tours before building solutions. We acknowledge a tension with Objectivity (Feeling with users vs analyzing data).",
"signals": "Dismissing user feedback that conflicts with internal assumptions or prioritizing data metrics over the actual lived experience of people."
},
{
"label": "Empowerment",
"meaning": "We give members the authority and resources to take charge of their own work. To us, this means we provide resources and authority to those closest to the problem. We acknowledge a tension with Control (Letting go means risking mistakes).",
"signals": "Delegating responsibility without giving the corresponding authority to make decisions or second-guessing every choice made by the team."
},
{
"label": "Equity",
"meaning": "We distribute resources based on need to ensure fair outcomes for everyone. To us, this means we use sliding scale pricing and offer scholarships to level the playing field. We acknowledge a tension with Equality (Different treatment vs same treatment).",
"signals": "Treating everyone exactly the same despite different starting points or refusing to accommodate specific needs under the guise of fairness."
},
{
"label": "Experimentation",
"meaning": "We encourage trying new things and learning from failure rather than sticking to the safe path. To us, this means we celebrate 'smart failures' as learning opportunities. We acknowledge a tension with Reliability (New things break).",
"signals": "Punishing members for experiments that do not succeed or requiring guaranteed success before approving any new initiative."
},
{
"label": "Fairness",
"meaning": "We apply rules and distribute resources in a just and impartial manner. To us, this means we use transparent salary bands and objective criteria for promotions. We acknowledge a tension with Favoritism (Treating everyone same vs helping friends).",
"signals": "Creating special rules for friends of the leadership or applying policies inconsistently depending on who is involved."
},
{
"label": "Fair Remuneration",
"meaning": "Workers are paid a fair wage that reflects the true value of their labor. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Expecting members to work for exposure or passion instead of paying a living wage for their labor."
},
{
"label": "Flexibility",
"meaning": "We accommodate the different needs and schedules of our members. To us, this means we allow remote work and asynchronous schedules. We acknowledge a tension with Predictability (Asynch work makes scheduling hard).",
"signals": "Penalizing members for using flexible work arrangements or judging commitment based on the number of hours physically present in the office."
},
{
"label": "Forkability",
"meaning": "Anyone has the right to take the work in a new direction if the current path fails. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Legally threatening or socially shunning a group that decides to take the project in a different direction due to disagreements."
},
{
"label": "Freedom",
"meaning": "We protect the liberty of individuals to express themselves and act as they choose. To us, this means we release our work under copyleft licenses. We acknowledge a tension with Security (Total freedom includes freedom to harm).",
"signals": "Censoring speech that is critical of the leadership or implementing surveillance tools to monitor member activity."
},
{
"label": "Freedom of Information",
"meaning": "Information is a public good that should not be hidden behind paywalls. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Locking important organizational knowledge behind paywalls or restricting access to documents based on arbitrary hierarchy."
},
{
"label": "Generosity",
"meaning": "We give freely of our time and resources to support the collective goal. To us, this means we share our surplus resources with aligned groups in need. We acknowledge a tension with Self-Preservation (Giving until it hurts).",
"signals": "Hoarding a surplus of resources while allied groups are struggling to survive or viewing every interaction as a transaction to be maximized."
},
{
"label": "Harm Reduction",
"meaning": "We focus on minimizing negative consequences associated with risky behaviors or systemic issues. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Insisting on abstinence-only approaches to risk or refusing to provide safety equipment because it might encourage risky behavior."
},
{
"label": "Holism",
"meaning": "We consider the whole person and the whole system rather than just isolated parts. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Ignoring the personal context of a member and treating them solely as a unit of production or optimizing one department at the expense of others."
},
{
"label": "Holocracy",
"meaning": "We organize around roles and circles rather than top-down managers. To us, this means we use circles and roles instead of managers and job titles. We acknowledge a tension with Clarity (Roles can be confusing compared to titles).",
"signals": "Reintroducing hidden hierarchies where influence is based on social capital rather than clear roles or ignoring governance processes."
},
{
"label": "Honesty",
"meaning": "We tell the truth even when it is difficult or uncomfortable. To us, this means we refuse to spin bad news and admit when we don't know the answer. We acknowledge a tension with Diplomacy (Blunt truth vs tact).",
"signals": "Hiding bad news from the membership to protect morale or giving vague non-answers when asked direct questions about challenges."
},
{
"label": "Horizontalism",
"meaning": "We organize as equals without bosses or top-down management structures. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Creating informal power structures where a shadow board makes the real decisions outside of the collective process."
},
{
"label": "Humility",
"meaning": "We recognize that we do not have all the answers and are open to learning from others. To us, this means Leaders eat last and credit the team for successes. We acknowledge a tension with Marketing (Selling yourself vs being modest).",
"signals": "Leaders taking credit for the team's success while blaming the team for failures or refusing to admit when they are wrong."
},
{
"label": "Inclusion",
"meaning": "We proactively welcome people who are often marginalized or excluded. To us, this means we proactively invite marginalized voices to the planning table. We acknowledge a tension with Exclusivity/Focus (Big tent vs niche group).",
"signals": "Holding meetings in spaces or at times that are inaccessible to working parents or people with different schedules."
},
{
"label": "Inclusivity",
"meaning": "We actively welcome people who have been historically marginalized or excluded. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Using jargon that alienates newcomers or failing to provide translation for members who speak different languages."
},
{
"label": "Independence",
"meaning": "We avoid reliance on any single funding source or authority figure. To us, this means we maintain a diversified funding base to avoid dependency on one donor. We acknowledge a tension with Resources (Going it alone means less help).",
"signals": "Relying on a single large donor who threatens to pull funding if the organization takes a political stance they dislike."
},
{
"label": "Innovation",
"meaning": "We value new ideas and creative solutions over tradition and established methods. To us, this means we reward novel approaches even if they disrupt current workflows. We acknowledge a tension with Tradition (New vs Old).",
"signals": "Shutting down new ideas with the excuse that things have always been done a certain way or refusing to allocate resources to test new concepts."
},
{
"label": "Integrity",
"meaning": "We align our actions with our stated values even when no one is watching. To us, this means we refuse contracts that violate our ethical standards, regardless of profit. We acknowledge a tension with Profit/Opportunity (Saying no to easy money).",
"signals": "Accepting a partnership that violates the group's core values because the financial offer is too good to refuse."
},
{
"label": "Interdependence",
"meaning": "We recognize that our survival and success are tied to the well-being of others. To us, this means we map our supply chain to ensure we support local ecosystems. We acknowledge a tension with Self-Sufficiency (Relying on others vs prepping).",
"signals": "Acting as if the organization exists in a vacuum and ignoring the impact of its actions on the surrounding community or ecosystem."
},
{
"label": "Interoperability",
"meaning": "Our tools and systems are designed to work seamlessly with other open systems. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Building walled gardens that trap users or deliberately breaking compatibility with other open tools to lock people in."
},
{
"label": "Intersectionality",
"meaning": "We consider how different forms of discrimination overlap to affect individuals. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Focusing on a single issue like class while ignoring how race and gender compound the experience of oppression for some members."
},
{
"label": "Justice",
"meaning": "We seek to repair past wrongs and create a fair system for the future. To us, this means we prioritize restorative processes over punitive ones. We acknowledge a tension with Mercy (Strict rules vs forgiveness).",
"signals": "Focusing on punishment and exclusion rather than healing and restoration or applying rules more strictly to marginalized members."
},
{
"label": "Knowledge Sharing",
"meaning": "We freely distribute information so that everyone can benefit from it. To us, this means we document everything in a public wiki. We acknowledge a tension with Intellectual Property (Open source vs patent).",
"signals": "Creating silos where information is hoarded to maintain power or refusing to document work so that others become dependent on a single expert."
},
{
"label": "Labor Rights",
"meaning": "We uphold the right of all workers to organize and advocate for their conditions. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Discouraging members from discussing their pay or working conditions or retaliating against those who try to organize."
},
{
"label": "Leadership",
"meaning": "We encourage everyone to step up and take initiative when needed. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Waiting for permission to solve an obvious problem or complaining about an issue without offering to help fix it."
},
{
"label": "Learning",
"meaning": "We embrace mistakes as opportunities to grow rather than failures to be punished. To us, this means we fund conference tickets and book clubs for members. We acknowledge a tension with Performance (Study time vs work time).",
"signals": "Viewing mistakes as a sign of incompetence rather than a learning opportunity or cutting the budget for personal development."
},
{
"label": "Liberty",
"meaning": "We oppose authoritarian control and support the freedom of the individual. To us, this means we defend the right of members to fork the project if they disagree. We acknowledge a tension with Order (Chaos vs rules).",
"signals": "Imposing strict rules on personal behavior that has no impact on the work or demanding ideological purity tests for membership."
},
{
"label": "Localism",
"meaning": "We prioritize local needs and resources over global or distant ones. To us, this means we source supplies from within a 50-mile radius. We acknowledge a tension with Globalism/Scale (Sourcing near vs sourcing cheap).",
"signals": "Importing goods that could be sourced locally to save a small amount of money or ignoring the needs of the immediate neighborhood."
},
{
"label": "Maintenance",
"meaning": "We value the unglamorous work of sustaining and fixing what we have already built. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Celebrating only new feature launches while neglecting the critical infrastructure that keeps the organization running."
},
{
"label": "Mentorship",
"meaning": "Experienced members actively guide and support newcomers. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Senior members refusing to spend time training juniors or creating a culture where asking for help is seen as weakness."
},
{
"label": "Meritocracy",
"meaning": "Influence is earned through active contribution and skill rather than status or wealth. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Rewarding people based on their confidence and social status rather than the actual quality and impact of their contributions."
},
{
"label": "Mutual Aid",
"meaning": "We engage in a reciprocal exchange of resources where everyone is both a giver and a receiver. To us, this means we practice 'solidarity, not charity' by exchanging resources directly. We acknowledge a tension with Charity (Solidarity vs Tax Write-offs).",
"signals": "Creating a transactional system where help is only given with the expectation of an immediate equivalent return."
},
{
"label": "Non-violence",
"meaning": "We reject physical and emotional violence as a means to achieve our goals. To us, this means we de-escalate conflicts and refuse to engage with law enforcement where possible. We acknowledge a tension with Self-Defense (Turning cheek vs fighting back).",
"signals": "Using aggressive language or intimidation tactics to silence opposition or justifying harm as necessary for the greater good."
},
{
"label": "Open Source",
"meaning": "We make our work freely available for others to use; study; and modify. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Releasing code without documentation or using a license that claims to be open but actually restricts commercial use or modification."
},
{
"label": "Openness",
"meaning": "We operate transparently and invite participation from the outside world. To us, this means All meetings are open to the public unless safety is at risk. We acknowledge a tension with Privacy/Security (Open doors let bugs in).",
"signals": "Conducting all real business in private messages and only using public meetings to rubber-stamp decisions that have already been made."
},
{
"label": "Participation",
"meaning": "Democracy requires the active engagement of all members in the decision-making process. To us, this means we require active contribution to maintain voting rights. We acknowledge a tension with Efficiency (Too many cooks).",
"signals": "Creating barriers to entry that make it difficult for new members to have a voice or ignoring the input of the broader membership."
}
]
}
+23
View File
@@ -32,6 +32,10 @@ export default {
nextButtonDisabled: {
control: { type: "boolean" },
},
backdropVariant: {
control: { type: "select" },
options: ["default", "loginYellow"],
},
currentStep: {
control: { type: "number", min: 1, max: 5 },
},
@@ -165,6 +169,25 @@ WithoutFooter.args = {
showNextButton: false,
};
export const LoginYellowBackdrop = Template.bind({});
LoginYellowBackdrop.args = {
isOpen: true,
title: "Horizontalism",
description: "Edit or add to this description to describe what this value means to your community.",
backdropVariant: "loginYellow",
children: (
<div className="space-y-4">
<p className="text-[var(--color-content-default-primary)]">
Core value detail body (yellow blurred overlay like Login).
</p>
</div>
),
showBackButton: false,
showNextButton: true,
nextButtonText: "Add Value",
nextButtonDisabled: false,
};
export const NextButtonDisabled = Template.bind({});
NextButtonDisabled.args = {
isOpen: true,
@@ -0,0 +1,42 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { screen, fireEvent, waitFor, within } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import { renderWithProviders } from "../utils/test-utils";
import { CoreValuesSelectScreen } from "../../app/create/screens/select/CoreValuesSelectScreen";
describe("CoreValuesSelectScreen", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("opens core value detail modal when a preset chip is clicked", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility"));
const dialog = await screen.findByRole("dialog");
expect(
within(dialog).getByRole("button", { name: "Add Value" }),
).toBeInTheDocument();
});
it("closes modal and reverts pending selection when Escape is pressed", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility"));
await screen.findByRole("dialog");
fireEvent.keyDown(document, { key: "Escape" });
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
});
it("saves details when Add Value is clicked", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility"));
const dialog = await screen.findByRole("dialog");
fireEvent.click(
within(dialog).getByRole("button", { name: "Add Value" }),
);
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
});
});
+12
View File
@@ -59,6 +59,18 @@ describe("Create", () => {
}
});
it("uses login yellow backdrop when backdropVariant is loginYellow", () => {
renderWithProviders(
<Create
{...defaultProps}
backdropVariant="loginYellow"
headerContent={<div>Header</div>}
/>,
);
const overlay = document.querySelector(".backdrop-blur-md");
expect(overlay).toBeInTheDocument();
});
it("renders footer buttons when provided", () => {
const onBack = vi.fn();
const onNext = vi.fn();
+24 -1
View File
@@ -39,6 +39,7 @@ describe("buildPublishPayload", () => {
],
},
],
coreValues: [],
});
});
@@ -58,6 +59,7 @@ describe("buildPublishPayload", () => {
entries: [{ title: "Community", body: "We organize locally." }],
},
],
coreValues: [],
});
});
@@ -71,7 +73,7 @@ describe("buildPublishPayload", () => {
const r = buildPublishPayload({ title: "T", sections });
expect(r.ok).toBe(true);
if (!r.ok) return;
expect(r.document).toEqual({ sections });
expect(r.document).toEqual({ sections, coreValues: [] });
});
it("filters invalid section entries from state.sections", () => {
@@ -86,8 +88,29 @@ describe("buildPublishPayload", () => {
if (!r.ok) return;
expect(r.document).toEqual({
sections: [{ categoryName: "Values", entries: [{ title: "A", body: "B" }] }],
coreValues: [],
});
});
it("includes coreValues from selected chips and detail text", () => {
const r = buildPublishPayload({
title: "T",
selectedCoreValueIds: ["1", "2"],
coreValuesChipsSnapshot: [
{ id: "1", label: "Alpha", state: "Selected" },
{ id: "2", label: "Beta", state: "Selected" },
],
coreValueDetailsByChipId: {
"1": { meaning: "m1", signals: "s1" },
},
});
expect(r.ok).toBe(true);
if (!r.ok) return;
expect(r.document.coreValues).toEqual([
{ chipId: "1", label: "Alpha", meaning: "m1", signals: "s1" },
{ chipId: "2", label: "Beta", meaning: "", signals: "" },
]);
});
});
describe("parseDocumentSectionsForDisplay", () => {
+19
View File
@@ -101,6 +101,25 @@ describe("createFlowStateSchema", () => {
});
expect(r.success).toBe(false);
});
it("accepts coreValueDetailsByChipId", () => {
const r = createFlowStateSchema.safeParse({
coreValueDetailsByChipId: {
"1": { meaning: "We care about access.", signals: "Blocking access." },
"uuid-here": { meaning: "", signals: "" },
},
});
expect(r.success).toBe(true);
});
it("rejects core value detail strings that are too long", () => {
const r = createFlowStateSchema.safeParse({
coreValueDetailsByChipId: {
"1": { meaning: "x".repeat(8001), signals: "y" },
},
});
expect(r.success).toBe(false);
});
});
describe("putDraftBodySchema", () => {